refactor(weather): add wind object to WeatherValue model

This commit is contained in:
2026-09-15 18:00:22 +03:00
parent 91a1e4498c
commit d47dcf69e6
10 changed files with 83 additions and 94 deletions
+3 -5
View File
@@ -7,17 +7,15 @@ from gallery.sketch.weather.model import (
Precipitation, Precipitation,
Sky, Sky,
WindDirection, WindDirection,
WindDirectionDeg,
) )
from gallery.util import root_path from gallery.util import root_path
def wind_direction_icon(wind_direction_deg: float) -> str: def wind_direction_icon(wind_direction: WindDirection | None) -> str:
wind_direction = WindDirectionDeg(wind_direction_deg).direction if wind_direction is None:
if wind_direction == WindDirection.CALM:
return "wind-calm" return "wind-calm"
else: else:
return f"wind-from-{wind_direction.name.lower()}" return f"wind-from-{wind_direction.lower()}"
def cloudness_icon(sky: Sky, date: datetime.datetime, period: str) -> list[str]: def cloudness_icon(sky: Sky, date: datetime.datetime, period: str) -> list[str]:
@@ -107,8 +107,8 @@ locale=request.state.language)}}{% endblock %}
{% for value in response.values %} {% for value in response.values %}
<td class="wind" <td class="wind"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="{{ value.wind_direction }}"> data-bs-title="{{ value.wind.direction }}">
<div class="wi-svg wi-s wi-{{value.wind_direction | wind_direction_icon}}" <div class="wi-svg wi-s wi-{{value.wind.direction | wind_direction_icon}}"
style="margin: 0 auto;">{{ 'wind-deg' | weather_icon_svg }}</div> style="margin: 0 auto;">{{ 'wind-deg' | weather_icon_svg }}</div>
</td> </td>
{% endfor %} {% endfor %}
@@ -123,11 +123,11 @@ locale=request.state.language)}}{% endblock %}
<tr> <tr>
{% for value in response.values %} {% for value in response.values %}
<td class="wind" <td class="wind"
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});"> style="background-color: rgba(128, 128, 128, {{value.wind.speed * 0.05}});">
<span class="speed">{{value.wind_speed}}</span> <span class="speed">{{value.wind.speed}}</span>
{% if value.wind_gust != value.wind_speed and value.wind_gust > 0 %} {% if value.wind.gust != value.wind.speed and value.wind.gust > 0 %}
<span class="gust"> <span class="gust">
({{value.wind_gust}}) ({{value.wind.gust}})
</span> </span>
{% endif %} {% endif %}
</td> </td>
+11 -2
View File
@@ -28,6 +28,15 @@ def format_date(date: datetime.date) -> str:
return f"{days + 1}-day" return f"{days + 1}-day"
def set_dict_nested_value(target: dict, key: str, value: Any):
parts = key.split(".")
for part in parts[:-1]:
if part not in target:
target[part] = {}
target = target[part]
target[parts[-1]] = value
class GismeteoApi(WeatherApi): class GismeteoApi(WeatherApi):
PROVIDER = "gismeteo" PROVIDER = "gismeteo"
@@ -46,7 +55,7 @@ class GismeteoApi(WeatherApi):
for index, value in enumerate(parser.parse_row(widget)): for index, value in enumerate(parser.parse_row(widget)):
while len(result) < index + 1: while len(result) < index + 1:
result.append({}) result.append({})
result[index][parser.KEY] = value set_dict_nested_value(result[index], parser.KEY, value)
values = [WeatherValue(**item) for item in result] values = [WeatherValue(**item) for item in result]
# group values by 3 hours # group values by 3 hours
if len(values) > 8: if len(values) > 8:
@@ -70,7 +79,7 @@ class GismeteoApi(WeatherApi):
for index, value in enumerate(parser.parse_row(widget)): for index, value in enumerate(parser.parse_row(widget)):
while len(result) < index + 1: while len(result) < index + 1:
result.append({}) result.append({})
result[index][parser.KEY] = value set_dict_nested_value(result[index], parser.KEY, value)
values = [WeatherValue(**item) for item in result] values = [WeatherValue(**item) for item in result]
return WeatherResponse( return WeatherResponse(
location=location or "n/a", location=location or "n/a",
+3 -3
View File
@@ -161,7 +161,7 @@ class TemperatureParser(RowParser[list[int]]):
class WindSpeedParser(RowParser[int]): class WindSpeedParser(RowParser[int]):
KEY = "wind_speed" KEY = "wind.speed"
def parse_row(self, tag: Tag) -> Iterable[int]: def parse_row(self, tag: Tag) -> Iterable[int]:
for item in tag.select(".widget-row-wind > .row-item > .wind-speed > speed-value"): for item in tag.select(".widget-row-wind > .row-item > .wind-speed > speed-value"):
@@ -169,7 +169,7 @@ class WindSpeedParser(RowParser[int]):
class WindGustParser(RowParser[int]): class WindGustParser(RowParser[int]):
KEY = "wind_gust" KEY = "wind.gust"
def parse_row(self, tag: Tag) -> Iterable[int]: def parse_row(self, tag: Tag) -> Iterable[int]:
for item in tag.select(".widget-row-wind > .row-item > .wind-gust"): for item in tag.select(".widget-row-wind > .row-item > .wind-gust"):
@@ -178,7 +178,7 @@ class WindGustParser(RowParser[int]):
class WindDirectionParser(RowParser[WindDirection]): class WindDirectionParser(RowParser[WindDirection]):
KEY = "wind_direction" KEY = "wind.direction"
def parse_row(self, tag: Tag) -> Iterable[float]: def parse_row(self, tag: Tag) -> Iterable[float]:
for item in tag.select(".widget-row-wind > .row-item > .wind-speed > .wind-direction"): for item in tag.select(".widget-row-wind > .row-item > .wind-speed > .wind-direction"):
+4 -3
View File
@@ -3,6 +3,7 @@ import datetime
from gallery.sketch.weather.model import Cloudness, Precipitation, WeatherValue from gallery.sketch.weather.model import Cloudness, Precipitation, WeatherValue
from gallery.sketch.weather.util import build_weather_value from gallery.sketch.weather.util import build_weather_value
from ...sketch.weather.parser import parse_wind_direction
from .openweather import ForecastItem from .openweather import ForecastItem
@@ -31,9 +32,9 @@ class ForecastItemParser:
# value.temperature = [round(item.main.temp_max), round(item.main.temp_min)] # value.temperature = [round(item.main.temp_max), round(item.main.temp_min)]
value.pressure = [round(item.main.pressure * 0.75006)] value.pressure = [round(item.main.pressure * 0.75006)]
value.humidity = item.main.humidity value.humidity = item.main.humidity
value.wind_speed = round(item.wind.speed) value.wind.speed = round(item.wind.speed)
value.wind_gust = round(item.wind.gust) value.wind.gust = round(item.wind.gust)
value.wind_direction = item.wind.deg value.wind.direction = parse_wind_direction(item.wind.deg)
value.sky.cloudness = self.CLOUDNESS_MAP.get(item.weather[0].description, Cloudness.CLEAR) value.sky.cloudness = self.CLOUDNESS_MAP.get(item.weather[0].description, Cloudness.CLEAR)
value.sky.precipitation = self.PRECIPITATION_MAP.get(item.weather[0].description, Precipitation.NO) value.sky.precipitation = self.PRECIPITATION_MAP.get(item.weather[0].description, Precipitation.NO)
if item.rain: if item.rain:
+3 -3
View File
@@ -133,13 +133,13 @@ def parse_page(page: BeautifulSoup) -> list[WeatherResponse]:
values[index].pressure = [int(cell.select_one(".p_0").text.strip())] values[index].pressure = [int(cell.select_one(".p_0").text.strip())]
case RowType.WIND_SPEED: case RowType.WIND_SPEED:
for index, cell in enumerate(row.select("td")[1:]): for index, cell in enumerate(row.select("td")[1:]):
values[index].wind_speed = _parse_wind_speed(cell) values[index].wind.speed = _parse_wind_speed(cell)
case RowType.WIND_GUST: case RowType.WIND_GUST:
for index, cell in enumerate(row.select("td")[1:]): for index, cell in enumerate(row.select("td")[1:]):
values[index].wind_gust = _parse_wind_speed(cell) values[index].wind.gust = _parse_wind_speed(cell)
case RowType.WIND_DIRECTION: case RowType.WIND_DIRECTION:
for index, cell in enumerate(row.select("td")[1:]): for index, cell in enumerate(row.select("td")[1:]):
values[index].wind_direction = parse_wind_direction(cell.text.strip()) values[index].wind.direction = parse_wind_direction(cell.text.strip())
case RowType.HUMIDITY: case RowType.HUMIDITY:
for index, cell in enumerate(row.select("td")[1:]): for index, cell in enumerate(row.select("td")[1:]):
values[index].humidity = int(cell.text.strip()) values[index].humidity = int(cell.text.strip())
+2 -2
View File
@@ -72,10 +72,10 @@ def parse_article(article: Tag, location: str) -> WeatherResponse:
value.temperature.append(temperature) value.temperature.append(temperature)
wind_speed_tag = temperature_tag.next_element.next_element wind_speed_tag = temperature_tag.next_element.next_element
wind_speed = int(wind_speed_tag.text.strip()) wind_speed = int(wind_speed_tag.text.strip())
value.wind_speed = wind_speed value.wind.speed = wind_speed
wind_direction_tag = wind_speed_tag.next_element.next_element.next_element.next_element wind_direction_tag = wind_speed_tag.next_element.next_element.next_element.next_element
wind_direction_str = wind_direction_tag.text.strip() wind_direction_str = wind_direction_tag.text.strip()
value.wind_direction = parse_wind_direction(wind_direction_str) value.wind.direction = parse_wind_direction(wind_direction_str)
humidity_tag = wind_direction_tag.next_element.next_element.next_element.next_element humidity_tag = wind_direction_tag.next_element.next_element.next_element.next_element
humidity = int(humidity_tag.text.strip().rstrip("%")) humidity = int(humidity_tag.text.strip().rstrip("%"))
value.humidity = humidity value.humidity = humidity
+5 -51
View File
@@ -58,7 +58,6 @@ class Sky(Model):
class WindDirection(StrEnum): class WindDirection(StrEnum):
CALM = auto()
N = auto() N = auto()
NE = auto() NE = auto()
E = auto() E = auto()
@@ -69,62 +68,17 @@ class WindDirection(StrEnum):
NW = auto() NW = auto()
class WindDirectionDeg(float): class Wind(Model):
@property speed: int
def direction(self) -> WindDirection: gust: int
return self.to_direction() direction: WindDirection | None = None
@property
def value(self) -> float:
return self
# pylint:disable=too-many-return-statements
def to_direction(self) -> WindDirection:
if self == -1:
return WindDirection.CALM
elif self > 337.5 or self <= 22.25:
return WindDirection.N
elif self <= 67.5:
return WindDirection.NE
elif self <= 112.5:
return WindDirection.E
elif self <= 157.5:
return WindDirection.SE
elif self <= 202.5:
return WindDirection.S
elif self <= 247.5:
return WindDirection.SW
elif self <= 292.5:
return WindDirection.W
elif self <= 337.5:
return WindDirection.NW
else:
raise ValueError(self)
@classmethod
def from_direction(cls, direction: WindDirection) -> Self:
return cls(
{
WindDirection.CALM: -1,
WindDirection.N: 0,
WindDirection.NE: 45,
WindDirection.E: 90,
WindDirection.SE: 135,
WindDirection.S: 180,
WindDirection.SW: 225,
WindDirection.W: 270,
WindDirection.NW: 315,
}[direction]
)
class WeatherValue(Model): class WeatherValue(Model):
date: datetime.datetime date: datetime.datetime
sky: Sky sky: Sky
wind: Wind
temperature: list[int] temperature: list[int]
wind_speed: int
wind_gust: int
wind_direction: float
precipitation: float precipitation: float
pressure: list[int] = Field(default=[]) pressure: list[int] = Field(default=[])
humidity: int = Field(default=-1) humidity: int = Field(default=-1)
+33 -8
View File
@@ -1,6 +1,6 @@
from typing import Self from typing import Self
from gallery.sketch.weather.model import WindDirection, WindDirectionDeg from gallery.sketch.weather.model import WindDirection
class ParseError(Exception): class ParseError(Exception):
@@ -9,11 +9,7 @@ class ParseError(Exception):
return cls("Invalid date string", date_str) return cls("Invalid date string", date_str)
WIND_DIRECTION_MAP: dict[str, WindDirection] = { WIND_DIRECTION_MAP: dict[str, WindDirection | None] = {
"—": WindDirection.CALM,
"": WindDirection.CALM,
"штиль": WindDirection.CALM,
"штл": WindDirection.CALM,
"с": WindDirection.N, "с": WindDirection.N,
"св": WindDirection.NE, "св": WindDirection.NE,
"в": WindDirection.E, "в": WindDirection.E,
@@ -25,5 +21,34 @@ WIND_DIRECTION_MAP: dict[str, WindDirection] = {
} }
def parse_wind_direction(value: str) -> WindDirectionDeg: # pylint:disable=too-many-return-statements
return WindDirectionDeg.from_direction(WIND_DIRECTION_MAP[value.replace("-", "").lower()]) def _parse_wind_direction_number(value: int | float) -> WindDirection | None:
if value > 337.5 or value <= 22.25:
return WindDirection.N
elif value <= 67.5:
return WindDirection.NE
elif value <= 112.5:
return WindDirection.E
elif value <= 157.5:
return WindDirection.SE
elif value <= 202.5:
return WindDirection.S
elif value <= 247.5:
return WindDirection.SW
elif value <= 292.5:
return WindDirection.W
elif value <= 337.5:
return WindDirection.NW
else:
return None
def _parse_wind_direction_str(value: str) -> WindDirection | None:
return WIND_DIRECTION_MAP.get(value.replace("-", "").lower())
def parse_wind_direction(value: str | int | float) -> WindDirection | None:
if isinstance(value, str):
return _parse_wind_direction_str(value)
else:
return _parse_wind_direction_number(value)
+13 -11
View File
@@ -1,8 +1,8 @@
import datetime import datetime
import statistics import statistics
from typing import Literal from typing import Counter, Literal
from .model import Cloudness, Precipitation, Sky, WeatherValue, WindDirectionDeg from .model import Cloudness, Precipitation, Sky, WeatherValue, Wind
MergeMode = Literal["interval", "average"] MergeMode = Literal["interval", "average"]
@@ -16,10 +16,12 @@ def build_weather_value(date: datetime.datetime) -> WeatherValue:
thunder=False, thunder=False,
fog=False, fog=False,
), ),
wind=Wind(
speed=0,
gust=0,
direction=None,
),
temperature=[], temperature=[],
wind_speed=0,
wind_gust=0,
wind_direction=WindDirectionDeg(-1),
precipitation=0, precipitation=0,
pressure=[], pressure=[],
humidity=-1, humidity=-1,
@@ -55,18 +57,18 @@ def merge_weather_values(
temperatures += value.temperature temperatures += value.temperature
pressures += value.pressure pressures += value.pressure
humidities.append(value.humidity) humidities.append(value.humidity)
wind_speeds.append(value.wind_speed) wind_speeds.append(value.wind.speed)
wind_gusts.append(value.wind_gust) wind_gusts.append(value.wind.gust)
wind_directions.append(value.wind_direction) wind_directions.append(value.wind.direction)
cloudnesses.append(value.sky.cloudness) cloudnesses.append(value.sky.cloudness)
precipitations.append(value.sky.precipitation) precipitations.append(value.sky.precipitation)
precipitation += value.precipitation precipitation += value.precipitation
result.temperature = _merge_values(temperatures, mode) result.temperature = _merge_values(temperatures, mode)
result.pressure = _merge_values(pressures, mode) result.pressure = _merge_values(pressures, mode)
result.humidity = round(statistics.mean(humidities)) result.humidity = round(statistics.mean(humidities))
result.wind_speed = round(statistics.mean(wind_speeds)) result.wind.speed = round(statistics.mean(wind_speeds))
result.wind_gust = round(statistics.mean(wind_gusts)) result.wind.gust = round(statistics.mean(wind_gusts))
result.wind_direction = statistics.mean(wind_directions) result.wind.direction = Counter(wind_directions).most_common(1)[0][0]
for item in cloudnesses: for item in cloudnesses:
if Cloudness.values.index(item) > Cloudness.values.index(result.sky.cloudness): if Cloudness.values.index(item) > Cloudness.values.index(result.sky.cloudness):
result.sky.cloudness = item result.sky.cloudness = item