refactor(openweather): rework openweather parser

This commit is contained in:
2026-09-17 22:03:24 +03:00
parent 2e9d04c10e
commit 6178c694f9
4 changed files with 95 additions and 85 deletions
+18 -33
View File
@@ -1,6 +1,6 @@
import datetime import datetime
import logging import logging
from collections import defaultdict from itertools import groupby
from os import environ from os import environ
from aiocache import cached from aiocache import cached
@@ -10,10 +10,9 @@ from gallery.sketch.weather.model import Location, WeatherResponse, WeatherValue
from gallery.sketch.weather.util import merge_weather_values from gallery.sketch.weather.util import merge_weather_values
from gallery.util import TimeUnit from gallery.util import TimeUnit
from .openweather import Forecast
from .openweather import Location as OpenWeatherLocation from .openweather import Location as OpenWeatherLocation
from .openweather import OpenWeather from .openweather import OpenWeather
from .parser import FORECAST_ITEM_PARSER from .parser import OpenWeatherForecastParser, OpenWeatherLocationParser
logger = logging.getLogger("openweather") logger = logging.getLogger("openweather")
@@ -21,6 +20,9 @@ logger = logging.getLogger("openweather")
class OpenWeatherApi(WeatherApi[OpenWeather]): class OpenWeatherApi(WeatherApi[OpenWeather]):
PROVIDER = "openweather" PROVIDER = "openweather"
forecast_parser = OpenWeatherForecastParser()
location_parser = OpenWeatherLocationParser()
def _build_source(self) -> OpenWeather: def _build_source(self) -> OpenWeather:
return OpenWeather(environ["OPENWEATHER_KEY"]) return OpenWeather(environ["OPENWEATHER_KEY"])
@@ -41,34 +43,17 @@ class OpenWeatherApi(WeatherApi[OpenWeather]):
alias="redis", alias="redis",
ttl=TimeUnit.HOUR, ttl=TimeUnit.HOUR,
) )
async def _get_location_forecast(self, location_id: str) -> Forecast: async def _get_location_forecast(self, location_id: str) -> list[WeatherValue]:
return await self.source.get_forecast(*self._parse_location(location_id)) content = await self.source.get_forecast(*self._parse_location(location_id))
return self.forecast_parser.parse(content)
async def find_locations(self, query: str) -> list[Location]: async def find_locations(self, query: str) -> list[Location]:
result = await self.source.find_locations(query) content = await self.source.find_locations(query)
return [ return self.location_parser.parse(content)
Location(
id=f"{item.lat}:{item.lon}",
name=item.name,
provider=self.provider,
lat=item.lat,
lon=item.lon,
country=item.country,
country_code=item.country.lower(),
district=item.state or "",
subdistrict="",
)
for item in 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:
location: OpenWeatherLocation = await self._get_location(location_id) location: OpenWeatherLocation = await self._get_location(location_id)
data: Forecast = await self._get_location_forecast(location_id) values = [value for value in await self._get_location_forecast(location_id) if value.date.date() == date]
values = []
for item in data.list:
value = FORECAST_ITEM_PARSER.parse(item)
if value.date.date() == date:
values.append(value)
return WeatherResponse( return WeatherResponse(
location=location.name, location=location.name,
date=date, date=date,
@@ -78,13 +63,13 @@ class OpenWeatherApi(WeatherApi[OpenWeather]):
async def get_days(self, location_id: str, days: int) -> WeatherResponse: async def get_days(self, location_id: str, days: int) -> WeatherResponse:
location: OpenWeatherLocation = await self._get_location(location_id) location: OpenWeatherLocation = await self._get_location(location_id)
data: Forecast = await self._get_location_forecast(location_id) values = [
values_by_date: dict[datetime.datetime, list[WeatherValue]] = defaultdict(list) merge_weather_values(datetime.datetime.combine(k, datetime.datetime.min.time()), list(g))
for item in data.list: for k, g in groupby(
value = FORECAST_ITEM_PARSER.parse(item) sorted(await self._get_location_forecast(location_id), key=lambda item: item.date.date()),
item_date = value.date.replace(hour=0, minute=0) lambda item: item.date.date(),
values_by_date[item_date].append(value) )
values = [merge_weather_values(date, values) for date, values in values_by_date.items()] ]
return WeatherResponse( return WeatherResponse(
location=location.name, location=location.name,
date=datetime.date.today(), date=datetime.date.today(),
+61 -23
View File
@@ -1,13 +1,20 @@
import datetime import datetime
from gallery.sketch.weather.model import Cloudness, Precipitation, WeatherValue from gallery.sketch.parse.core import Parser
from gallery.sketch.weather.util import build_weather_value from gallery.sketch.weather.model import (
Cloudness,
Location,
Precipitation,
WeatherValue,
)
from gallery.sketch.weather.parser import parse_wind_direction
from gallery.sketch.weather.util import convert_hPa_to_mmHg
from ...sketch.weather.parser import parse_wind_direction from .openweather import Forecast, ForecastItem
from .openweather import ForecastItem from .openweather import Location as OpenWeatherLocation
class ForecastItemParser: class ForecastItemParser(Parser[ForecastItem, list[WeatherValue]]):
CLOUDNESS_MAP: dict[str, Cloudness] = { CLOUDNESS_MAP: dict[str, Cloudness] = {
"clear sky": Cloudness.CLEAR, "clear sky": Cloudness.CLEAR,
"few clouds": Cloudness.PARTLY_CLOUDY, "few clouds": Cloudness.PARTLY_CLOUDY,
@@ -23,23 +30,54 @@ class ForecastItemParser:
"heavy rain": Precipitation.SHOWER, "heavy rain": Precipitation.SHOWER,
} }
def parse(self, item: ForecastItem) -> WeatherValue: def parse(self, data: ForecastItem) -> WeatherValue:
item_date = datetime.datetime.fromtimestamp(item.dt, datetime.UTC) date = (
item_date = item_date.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).replace(tzinfo=None) datetime.datetime.fromtimestamp(data.dt, datetime.UTC)
value = build_weather_value(item_date) .replace(tzinfo=datetime.timezone.utc)
# TODO parse temperature interval flag .astimezone(tz=None)
value.temperature = [round(item.main.temp)] .replace(tzinfo=None)
# value.temperature = [round(item.main.temp_max), round(item.main.temp_min)] )
value.pressure = [round(item.main.pressure * 0.75006)] return WeatherValue.model_validate(
value.humidity = item.main.humidity {
value.wind.speed = round(item.wind.speed) "date": date,
value.wind.gust = round(item.wind.gust) "sky": {
value.wind.direction = parse_wind_direction(item.wind.deg) "cloudness": self.CLOUDNESS_MAP.get(data.weather[0].description, Cloudness.CLEAR),
value.sky.cloudness = self.CLOUDNESS_MAP.get(item.weather[0].description, Cloudness.CLEAR) "precipitation": self.PRECIPITATION_MAP.get(data.weather[0].description, Precipitation.NO),
value.sky.precipitation = self.PRECIPITATION_MAP.get(item.weather[0].description, Precipitation.NO) },
if item.rain: "wind": {
value.precipitation = round(item.rain.interval_3h, 1) "speed": round(data.wind.speed),
return value "gust": round(data.wind.gust),
"direction": parse_wind_direction(data.wind.deg),
},
"temperature": [round(data.main.temp)], # [round(data.main.temp_max), round(data.main.temp_min)]
"precipitation": round(data.rain.interval_3h, 1) if data.rain else 0,
"pressure": [convert_hPa_to_mmHg(data.main.pressure)],
"humidity": data.main.humidity,
}
)
FORECAST_ITEM_PARSER = ForecastItemParser() class OpenWeatherForecastParser(Parser[Forecast, list[WeatherValue]]):
item_parser = ForecastItemParser()
def parse(self, data: Forecast) -> list[WeatherValue]:
return [self.item_parser.parse(item) for item in data.list]
class OpenWeatherLocationParser(Parser[list[OpenWeatherLocation], list[Location]]):
def parse(self, data: list[OpenWeatherLocation]) -> list[Location]:
return [
Location(
id=f"{item.lat}:{item.lon}",
name=item.name,
provider="openweather",
lat=item.lat,
lon=item.lon,
country=item.country,
country_code=item.country.lower(),
district=item.state or "",
subdistrict="",
)
for item in data
]
+6 -6
View File
@@ -1,6 +1,6 @@
import datetime import datetime
from enum import StrEnum, auto from enum import StrEnum, auto
from typing import Self from typing import Literal, Self
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -76,16 +76,16 @@ class Wind(Model):
class WeatherValue(Model): class WeatherValue(Model):
date: datetime.datetime date: datetime.datetime
sky: Sky sky: Sky = Field(default_factory=Sky)
wind: Wind wind: Wind = Field(default_factory=Wind)
temperature: list[int] temperature: list[int] = Field(default_factory=list)
precipitation: float = 0 precipitation: float = 0
pressure: list[int] = Field(default=[]) pressure: list[int] = Field(default_factory=list)
humidity: int = -1 humidity: int = -1
class WeatherResponse(Model): class WeatherResponse(Model):
location: str location: str
date: datetime.date date: datetime.date
period: str period: Literal["day", "days"] = "day"
values: list[WeatherValue] values: list[WeatherValue]
+10 -23
View File
@@ -2,32 +2,11 @@ import datetime
import statistics import statistics
from typing import Counter, Literal from typing import Counter, Literal
from .model import Cloudness, Precipitation, Sky, WeatherValue, Wind from .model import Cloudness, Precipitation, WeatherValue
MergeMode = Literal["interval", "average"] MergeMode = Literal["interval", "average"]
def build_weather_value(date: datetime.datetime | None = None) -> WeatherValue:
return WeatherValue(
date=date or datetime.datetime.now(),
sky=Sky(
cloudness=Cloudness.CLEAR,
precipitation=Precipitation.NO,
thunder=False,
fog=False,
),
wind=Wind(
speed=0,
gust=0,
direction=None,
),
temperature=[],
precipitation=0,
pressure=[],
humidity=-1,
)
def _merge_values(values: list[int], mode: MergeMode) -> list[int]: def _merge_values(values: list[int], mode: MergeMode) -> list[int]:
if mode == "interval": if mode == "interval":
if (max_value := max(values)) == (min_value := min(values)): if (max_value := max(values)) == (min_value := min(values)):
@@ -43,7 +22,7 @@ def merge_weather_values(
values: list[WeatherValue], values: list[WeatherValue],
mode: MergeMode = "interval", mode: MergeMode = "interval",
) -> WeatherValue: ) -> WeatherValue:
result = build_weather_value(date) result = WeatherValue(date=date)
temperatures = [] temperatures = []
pressures = [] pressures = []
humidities = [] humidities = []
@@ -70,10 +49,18 @@ def merge_weather_values(
result.wind.gust = round(statistics.mean(wind_gusts)) result.wind.gust = round(statistics.mean(wind_gusts))
result.wind.direction = Counter(wind_directions).most_common(1)[0][0] result.wind.direction = Counter(wind_directions).most_common(1)[0][0]
for item in cloudnesses: for item in cloudnesses:
# TODO: Instance of 'FieldInfo' has no 'cloudness' member (no-member)
# pylint:disable=no-member
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
for item in precipitations: for item in precipitations:
# TODO: Instance of 'FieldInfo' has no 'precipitation' member (no-member)
# pylint:disable=no-member
if Precipitation.values.index(item) > Precipitation.values.index(result.sky.precipitation): if Precipitation.values.index(item) > Precipitation.values.index(result.sky.precipitation):
result.sky.precipitation = item result.sky.precipitation = item
result.precipitation = precipitation result.precipitation = precipitation
return result return result
def convert_hPa_to_mmHg(value: int) -> int:
return round(value / 1.333)