3 Commits

Author SHA1 Message Date
8012d9b8ed fix(gismeteo): add max_date validation 2026-06-14 10:23:38 +03:00
3a6faa85be fix(gismeteo): add new variants to sky parser 2026-06-14 10:23:13 +03:00
1af61aa3c7 feat(easel): add sky details tooltip 2026-06-14 10:22:43 +03:00
5 changed files with 39 additions and 9 deletions

View File

@@ -54,7 +54,7 @@
</tr> </tr>
<tr> <tr>
{% for value in response.values %} {% for value in response.values %}
<td class="cloudness"> <td class="cloudness" data-bs-toggle="tooltip" data-bs-title="{{ value.sky }}">
{% for icon in value.sky | cloudness_icon %} {% for icon in value.sky | cloudness_icon %}
<div class="icon">{{icon}}</div> <div class="icon">{{icon}}</div>
{% endfor %} {% endfor %}

View File

@@ -4,6 +4,7 @@ import logging
from typing import Any from typing import Any
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from fastapi import HTTPException, status
from gallery.sketch.source import ApiSource from gallery.sketch.source import ApiSource
from gallery.sketch.weather.api import WeatherApi from gallery.sketch.weather.api import WeatherApi
@@ -99,6 +100,11 @@ class GismeteoApi(WeatherApi):
return result return result
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse: async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
max_date = datetime.date.today() + datetime.timedelta(days=9)
if date > max_date:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail={"max_date": max_date.strftime("%Y-%m-%d")}
)
data = await self.SOURCE.request(f"weather-{location_id}/{datehelp.dump(date)}") data = await self.SOURCE.request(f"weather-{location_id}/{datehelp.dump(date)}")
return self._parse_oneday(date, data) return self._parse_oneday(date, data)

View File

@@ -1,4 +1,5 @@
import datetime import datetime
import logging
import re import re
from typing import Iterable from typing import Iterable
@@ -15,6 +16,8 @@ from gallery.sketch.weather.model import (
from .core import BaseWidgetParser, RowParser from .core import BaseWidgetParser, RowParser
logger = logging.getLogger("gismeteo")
ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items") ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items")
DAYS_PARSER = BaseWidgetParser(".widget.widget-days .widget-items") DAYS_PARSER = BaseWidgetParser(".widget.widget-days .widget-items")
@@ -57,6 +60,7 @@ class SkyParser(RowParser[Sky]):
CLOUDNESS_MAP: dict[str, Cloudness] = { CLOUDNESS_MAP: dict[str, Cloudness] = {
"ясно": Cloudness.CLEAR, "ясно": Cloudness.CLEAR,
"безоблачно": Cloudness.CLEAR,
"малооблачно": Cloudness.PARTLY_CLOUDY, "малооблачно": Cloudness.PARTLY_CLOUDY,
"облачно": Cloudness.CLOUDY, "облачно": Cloudness.CLOUDY,
"пасмурно": Cloudness.MAINLY_CLOUDY, "пасмурно": Cloudness.MAINLY_CLOUDY,
@@ -64,8 +68,10 @@ class SkyParser(RowParser[Sky]):
PRECIPITATION_MAP: dict[str, Precipitation] = { PRECIPITATION_MAP: dict[str, Precipitation] = {
"без осадков": Precipitation.NO, "без осадков": Precipitation.NO,
"небольшой дождь": Precipitation.SMALL_RAIN, "небольшой дождь": Precipitation.SMALL_RAIN, # TODO: remove it?
"небольшой дождь": Precipitation.SMALL_RAIN,
"сильный дождь": Precipitation.HEAVY_RAIN, "сильный дождь": Precipitation.HEAVY_RAIN,
"ливневый дождь": Precipitation.SHOWER,
"дождь": Precipitation.RAIN, "дождь": Precipitation.RAIN,
"ливень": Precipitation.SHOWER, "ливень": Precipitation.SHOWER,
"снег": Precipitation.SNOW, "снег": Precipitation.SNOW,
@@ -78,22 +84,35 @@ class SkyParser(RowParser[Sky]):
"небольшой мокрый снег": Precipitation.SNOW, "небольшой мокрый снег": Precipitation.SNOW,
} }
THUNDER = "гроза"
FOG = "дымка"
def parse_row(self, tag: Tag) -> Iterable[Sky]: def parse_row(self, tag: Tag) -> Iterable[Sky]:
for item in tag.select(".widget-row[data-row=icon-tooltip] > .row-item"): for item in tag.select(".widget-row[data-row=icon-tooltip] > .row-item"):
sky_str = item.attrs["data-tooltip"] sky_str = item.attrs["data-tooltip"]
values = {item.strip().lower() for item in sky_str.split(",")} values = {item.strip().lower() for item in sky_str.split(",")}
cloudness = Cloudness.CLEAR cloudness = Cloudness.CLEAR
precipitation = Precipitation.NO precipitation = Precipitation.NO
thunder = "гроза" in values thunder = False
fog = "дымка" in values fog = False
if self.THUNDER in values:
thunder = True
values.remove(self.THUNDER)
if self.FOG in values:
fog = True
values.remove(self.FOG)
for k, v in self.CLOUDNESS_MAP.items(): for k, v in self.CLOUDNESS_MAP.items():
if k in values: if k in values:
cloudness = v cloudness = v
values.remove(k)
break break
for k, v in self.PRECIPITATION_MAP.items(): for k, v in self.PRECIPITATION_MAP.items():
if k in values: if k in values:
precipitation = v precipitation = v
values.remove(k)
break break
if values:
logger.warning("unknown sky values: %s:", values)
yield Sky( yield Sky(
cloudness=cloudness, cloudness=cloudness,
precipitation=precipitation, precipitation=precipitation,

View File

@@ -1,5 +1,10 @@
import "./main.scss"; import * as bootstrap from "bootstrap";
import "bootstrap";
import "./theme";
import "./language";
import "./components"; import "./components";
import "./language";
import "./main.scss";
import "./theme";
document.addEventListener("DOMContentLoaded", (event) => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl));
});

View File

@@ -37,7 +37,7 @@
//@import "bootstrap/scss/close"; //@import "bootstrap/scss/close";
//@import "bootstrap/scss/toasts"; //@import "bootstrap/scss/toasts";
//@import "bootstrap/scss/modal"; //@import "bootstrap/scss/modal";
//@import "bootstrap/scss/tooltip"; @import "bootstrap/scss/tooltip";
//@import "bootstrap/scss/popover"; //@import "bootstrap/scss/popover";
//@import "bootstrap/scss/carousel"; //@import "bootstrap/scss/carousel";
//@import "bootstrap/scss/spinners"; //@import "bootstrap/scss/spinners";