Files
gallery/gallery/painting/gismeteo/parser.py
T

223 lines
7.4 KiB
Python

import datetime
import logging
import re
from typing import Literal, NamedTuple
from bs4 import BeautifulSoup, Tag
from gallery.sketch.parse.core import ParseError, Parser
from gallery.sketch.parse.date import parse_date
from gallery.sketch.parse.html import TableParser, TableValueParser
from gallery.sketch.weather.model import (
Cloudness,
Precipitation,
Sky,
WeatherResponse,
WeatherValue,
WindDirection,
)
from gallery.sketch.weather.parser import parse_wind_direction
from gallery.sketch.weather.util import merge_weather_values
logger = logging.getLogger("gismeteo")
class LocationNameParser(Parser[str, str]):
PATTERN = re.compile('{"ru":{"city":{"name":"(.*?)"')
def parse(self, data: str) -> str:
match = self.PATTERN.search(data)
if match:
return match.group(1)
raise ParseError("Location not found")
class TimeParser(TableValueParser[datetime.datetime]):
selector = ".widget-row.widget-row-datetime-time > .row-item > time-value"
model_key = "date"
def extract(self, data: Tag) -> datetime.datetime:
timestamp = int(data.attrs["timestamp"])
return datetime.datetime.fromtimestamp(timestamp)
class DateParser(TableValueParser[datetime.datetime]):
selector = ".widget-row.widget-row-date > .row-item"
model_key = "date"
def extract(self, data: Tag) -> datetime.datetime:
return parse_date(data.text)
class SkyParser(TableValueParser[Sky]):
selector = ".widget-row[data-row=icon-tooltip] > .row-item"
model_key = "sky"
CLOUDNESS_MAP: dict[str, Cloudness] = {
"ясно": Cloudness.CLEAR,
"безоблачно": Cloudness.CLEAR,
"малооблачно": Cloudness.PARTLY_CLOUDY,
"облачно": Cloudness.CLOUDY,
"пасмурно": Cloudness.MAINLY_CLOUDY,
}
PRECIPITATION_MAP: dict[str, Precipitation] = {
"без осадков": Precipitation.NO,
"без существенных осадков": Precipitation.NO,
"небольшой дождь": Precipitation.SMALL_RAIN,
"сильный дождь": Precipitation.HEAVY_RAIN,
"очень сильный дождь": Precipitation.HEAVY_RAIN,
"ливневый дождь": Precipitation.SHOWER,
"дождь": Precipitation.RAIN,
"ливень": Precipitation.SHOWER,
"снег": Precipitation.SNOW,
"небольшой снег": Precipitation.SNOW,
"сильный снег": Precipitation.HEAVY_SNOW,
"мокрый снег": Precipitation.SNOW,
"снег с дождём": Precipitation.SNOW,
"сильный снег с дождём": Precipitation.HEAVY_SNOW,
"небольшой снег с дождём": Precipitation.SNOW,
"небольшой мокрый снег": Precipitation.SNOW,
"град": Precipitation.HAIL,
}
THUNDER = {"гроза"}
FOG = {"дымка", "туман"}
@classmethod
def _detect_flag(cls, values: set[str], flag_values: set[str]) -> tuple[set[str], bool]:
result_values = values - flag_values
return result_values, len(result_values) < len(values)
def extract(self, data: Tag) -> Sky:
sky_str = data.attrs["data-tooltip"]
values = {item.strip().lower() for item in sky_str.split(",")}
cloudness = Cloudness.CLEAR
precipitation = Precipitation.NO
values, thunder = self._detect_flag(values, self.THUNDER)
values, fog = self._detect_flag(values, self.FOG)
for k, v in self.CLOUDNESS_MAP.items():
if k in values:
cloudness = v
values.remove(k)
break
for k, v in self.PRECIPITATION_MAP.items():
if k in values:
precipitation = v
values.remove(k)
break
if values:
logger.warning("unknown sky values: %s:", values)
return Sky(
cloudness=cloudness,
precipitation=precipitation,
thunder=thunder,
fog=fog,
)
class TemperatureParser(TableValueParser[list[int]]):
selector = ".widget-row-chart[data-row=temperature-air] > .chart > .values > .value"
model_key = "temperature"
def extract(self, data: Tag) -> list[int]:
return [int(value.attrs["value"]) for value in data.select("temperature-value")]
class WindSpeedParser(TableValueParser[int]):
selector = ".widget-row-wind > .row-item > .wind-speed"
model_key = "wind.speed"
def extract(self, data: Tag) -> int:
value = data.select_one("speed-value")
return int(value.attrs["value"]) if value else 0
class WindGustParser(TableValueParser[int]):
selector = ".widget-row-wind > .row-item > .wind-gust"
model_key = "wind.gust"
def extract(self, data: Tag) -> int:
value = data.select_one("speed-value")
return int(value.attrs["value"]) if value else 0
class WindDirectionParser(TableValueParser[WindDirection | None]):
selector = ".widget-row-wind > .row-item > .wind-speed > .wind-direction"
model_key = "wind.direction"
def extract(self, data: Tag) -> WindDirection | None:
return parse_wind_direction(data.text.strip())
class PrecipitationParser(TableValueParser[float]):
selector = ".widget-row[data-row=precipitation-bars] > .row-item"
model_key = "precipitation"
def extract(self, data: Tag) -> float:
value = data.select_one("precipitation-value")
return float(value.attrs["value"]) if value else 0
class PressureParser(TableValueParser[list[int]]):
selector = ".widget-row-chart[data-row=pressure] > .chart > .values > .value"
model_key = "pressure"
def extract(self, data: Tag) -> list[int]:
return [int(value.attrs["value"]) for value in data.select("pressure-value")]
class HumidityParser(TableValueParser[int]):
selector = ".widget-row[data-row=humidity] > .row-item, .widget-row[data-row=humidity-avg] > .row-item"
model_key = "humidity"
def extract(self, data: Tag) -> int:
return int(data.text)
class GismeteoTableParser(TableParser[WeatherValue]):
selector = ".widget .widget-items"
value_parsers = [
TimeParser(),
DateParser(),
SkyParser(),
TemperatureParser(),
WindSpeedParser(),
WindGustParser(),
WindDirectionParser(),
PrecipitationParser(),
PressureParser(),
HumidityParser(),
]
def create_model(self, data: dict) -> WeatherValue:
return WeatherValue.model_validate(data)
class Context(NamedTuple):
date: datetime.date
period: Literal["day", "days"]
data: str
values: int | None = None
class GismeteoParser(Parser[Context, WeatherResponse]):
location_parser = LocationNameParser()
table_parser = GismeteoTableParser()
def parse(self, data: Context) -> WeatherResponse:
location = self.location_parser.parse(data.data)
soup = BeautifulSoup(data.data, features="html.parser")
values = self.table_parser.parse(soup)
if data.values and len(values) > data.values:
n = int(len((values)) / data.values)
values = [
merge_weather_values(values[i].date, values[i : i + n], "average") for i in range(0, len(values), n)
]
return WeatherResponse(
location=location,
date=data.date,
period=data.period,
values=values,
)