diff options
Diffstat (limited to 'searx/weather.py')
| -rw-r--r-- | searx/weather.py | 195 |
1 files changed, 101 insertions, 94 deletions
diff --git a/searx/weather.py b/searx/weather.py index c8f3cf973..30b5bfeee 100644 --- a/searx/weather.py +++ b/searx/weather.py @@ -14,11 +14,10 @@ __all__ = [ "GeoLocation", ] -import typing +import typing as t import base64 import datetime -import dataclasses import zoneinfo from urllib.parse import quote_plus @@ -27,7 +26,8 @@ import babel import babel.numbers import babel.dates import babel.languages -import flask_babel +import flask_babel # pyright: ignore[reportMissingTypeStubs] +import msgspec from searx import network from searx.cache import ExpireCache, ExpireCacheCfg @@ -120,8 +120,7 @@ def symbol_url(condition: "WeatherConditionType") -> str | None: return data_url -@dataclasses.dataclass -class GeoLocation: +class GeoLocation(msgspec.Struct, kw_only=True): """Minimal implementation of Geocoding.""" # The type definition was based on the properties from the geocoding API of @@ -176,6 +175,8 @@ class GeoLocation: ctx = "weather_geolocation_by_query" cache = get_WEATHER_DATA_CACHE() + # {'name': 'Berlin', 'latitude': 52.52437, 'longitude': 13.41053, + # 'elevation': 74.0, 'country_code': 'DE', 'timezone': 'Europe/Berlin'} geo_props = cache.get(search_term, ctx=ctx) if not geo_props: @@ -194,15 +195,14 @@ class GeoLocation: if not results: raise ValueError(f"unknown geo location: '{search_term}'") location = results[0] - return {field.name: location[field.name] for field in dataclasses.fields(cls)} + return {field_name: location[field_name] for field_name in cls.__struct_fields__} -DateTimeFormats = typing.Literal["full", "long", "medium", "short"] -DateTimeLocaleTypes = typing.Literal["UI"] +DateTimeFormats = t.Literal["full", "long", "medium", "short"] +DateTimeLocaleTypes = t.Literal["UI"] -@typing.final -class DateTime: +class DateTime(msgspec.Struct): """Class to represent date & time. Essentially, it is a wrapper that conveniently combines :py:obj:`datetime.datetime` and :py:obj:`babel.dates.format_datetime`. A conversion of time zones is not @@ -216,8 +216,7 @@ class DateTime: as the value for the ``locale``. """ - def __init__(self, time: datetime.datetime): - self.datetime = time + datetime: datetime.datetime def __str__(self): return self.l10n() @@ -252,36 +251,35 @@ class DateTime: return babel.dates.format_date(self.datetime, format=fmt, locale=locale) -@typing.final -class Temperature: +TemperatureUnits: t.TypeAlias = t.Literal["°C", "°F", "K"] + + +class Temperature(msgspec.Struct, kw_only=True): """Class for converting temperature units and for string representation of measured values.""" - si_name = "Q11579" + val: float + unit: TemperatureUnits - Units = typing.Literal["°C", "°F", "K"] - """Supported temperature units.""" + si_name: t.ClassVar[str] = "Q11579" + units: t.ClassVar[list[str]] = list(t.get_args(TemperatureUnits)) - units = list(typing.get_args(Units)) - - def __init__(self, value: float, unit: Units): - if unit not in self.units: - raise ValueError(f"invalid unit: {unit}") - self.si: float = convert_to_si( # pylint: disable=invalid-name - si_name=self.si_name, - symbol=unit, - value=value, - ) + def __post_init__(self): + if self.unit not in self.units: + raise ValueError(f"invalid unit: {self.unit}") def __str__(self): return self.l10n() - def value(self, unit: Units) -> float: - return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si) + def value(self, unit: TemperatureUnits) -> float: + if unit == self.unit: + return self.val + si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val) + return convert_from_si(si_name=self.si_name, symbol=unit, value=si_val) def l10n( self, - unit: Units | None = None, + unit: TemperatureUnits | None = None, locale: babel.Locale | GeoLocation | None = None, template: str = "{value} {unit}", num_pattern: str = "#,##0", @@ -320,33 +318,35 @@ class Temperature: return template.format(value=val_str, unit=unit) -@typing.final -class Pressure: +PressureUnits: t.TypeAlias = t.Literal["Pa", "hPa", "cm Hg", "bar"] + + +class Pressure(msgspec.Struct, kw_only=True): """Class for converting pressure units and for string representation of measured values.""" - si_name = "Q44395" - - Units = typing.Literal["Pa", "hPa", "cm Hg", "bar"] - """Supported units.""" + val: float + unit: PressureUnits - units = list(typing.get_args(Units)) + si_name: t.ClassVar[str] = "Q44395" + units: t.ClassVar[list[str]] = list(t.get_args(PressureUnits)) - def __init__(self, value: float, unit: Units): - if unit not in self.units: - raise ValueError(f"invalid unit: {unit}") - # pylint: disable=invalid-name - self.si: float = convert_to_si(si_name=self.si_name, symbol=unit, value=value) + def __post_init__(self): + if self.unit not in self.units: + raise ValueError(f"invalid unit: {self.unit}") def __str__(self): return self.l10n() - def value(self, unit: Units) -> float: - return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si) + def value(self, unit: PressureUnits) -> float: + if unit == self.unit: + return self.val + si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val) + return convert_from_si(si_name=self.si_name, symbol=unit, value=si_val) def l10n( self, - unit: Units | None = None, + unit: PressureUnits | None = None, locale: babel.Locale | GeoLocation | None = None, template: str = "{value} {unit}", num_pattern: str = "#,##0", @@ -363,8 +363,10 @@ class Pressure: return template.format(value=val_str, unit=unit) -@typing.final -class WindSpeed: +WindSpeedUnits: t.TypeAlias = t.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"] + + +class WindSpeed(msgspec.Struct, kw_only=True): """Class for converting speed or velocity units and for string representation of measured values. @@ -375,28 +377,28 @@ class WindSpeed: (55.6 m/s) """ - si_name = "Q182429" - - Units = typing.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"] - """Supported units.""" + val: float + unit: WindSpeedUnits - units = list(typing.get_args(Units)) + si_name: t.ClassVar[str] = "Q182429" + units: t.ClassVar[list[str]] = list(t.get_args(WindSpeedUnits)) - def __init__(self, value: float, unit: Units): - if unit not in self.units: - raise ValueError(f"invalid unit: {unit}") - # pylint: disable=invalid-name - self.si: float = convert_to_si(si_name=self.si_name, symbol=unit, value=value) + def __post_init__(self): + if self.unit not in self.units: + raise ValueError(f"invalid unit: {self.unit}") def __str__(self): return self.l10n() - def value(self, unit: Units) -> float: - return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si) + def value(self, unit: WindSpeedUnits) -> float: + if unit == self.unit: + return self.val + si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val) + return convert_from_si(si_name=self.si_name, symbol=unit, value=si_val) def l10n( self, - unit: Units | None = None, + unit: WindSpeedUnits | None = None, locale: babel.Locale | GeoLocation | None = None, template: str = "{value} {unit}", num_pattern: str = "#,##0", @@ -413,23 +415,23 @@ class WindSpeed: return template.format(value=val_str, unit=unit) -@typing.final -class RelativeHumidity: - """Amount of relative humidity in the air. The unit is ``%``""" +RelativeHumidityUnits: t.TypeAlias = t.Literal["%"] + - Units = typing.Literal["%"] - """Supported unit.""" +class RelativeHumidity(msgspec.Struct): + """Amount of relative humidity in the air. The unit is ``%``""" - units = list(typing.get_args(Units)) + val: float - def __init__(self, humidity: float): - self.humidity = humidity + # there exists only one unit (%) --> set "%" as the final value (constant) + unit: t.ClassVar["t.Final[RelativeHumidityUnits]"] = "%" + units: t.ClassVar[list[str]] = list(t.get_args(RelativeHumidityUnits)) def __str__(self): return self.l10n() def value(self) -> float: - return self.humidity + return self.val def l10n( self, @@ -447,45 +449,50 @@ class RelativeHumidity: return template.format(value=val_str, unit=unit) -@typing.final -class Compass: - """Class for converting compass points and azimuth values (360°)""" +CompassPoint: t.TypeAlias = t.Literal[ + "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" +] +"""Compass point type definition""" - Units = typing.Literal["°", "Point"] +CompassUnits: t.TypeAlias = t.Literal["°", "Point"] - Point = typing.Literal[ - "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" - ] - """Compass point type definition""" - TURN = 360.0 +class Compass(msgspec.Struct): + """Class for converting compass points and azimuth values (360°)""" + + val: "float | int | CompassPoint" + unit: CompassUnits = "°" + + TURN: t.ClassVar[float] = 360.0 """Full turn (360°)""" - POINTS = list(typing.get_args(Point)) + POINTS: t.ClassVar[list[CompassPoint]] = list(t.get_args(CompassPoint)) """Compass points.""" - RANGE = TURN / len(POINTS) + RANGE: t.ClassVar[float] = TURN / len(POINTS) """Angle sector of a compass point""" - def __init__(self, azimuth: float | int | Point): - if isinstance(azimuth, str): - if azimuth not in self.POINTS: - raise ValueError(f"Invalid compass point: {azimuth}") - azimuth = self.POINTS.index(azimuth) * self.RANGE - self.azimuth = azimuth % self.TURN + def __post_init__(self): + if isinstance(self.val, str): + if self.val not in self.POINTS: + raise ValueError(f"Invalid compass point: {self.val}") + self.val = self.POINTS.index(self.val) * self.RANGE + + self.val = self.val % self.TURN + self.unit = "°" def __str__(self): return self.l10n() - def value(self, unit: Units): - if unit == "Point": - return self.point(self.azimuth) + def value(self, unit: CompassUnits): + if unit == "Point" and isinstance(self.val, float): + return self.point(self.val) if unit == "°": - return self.azimuth + return self.val raise ValueError(f"unknown unit: {unit}") @classmethod - def point(cls, azimuth: float | int) -> Point: + def point(cls, azimuth: float | int) -> CompassPoint: """Returns the compass point to an azimuth value.""" azimuth = azimuth % cls.TURN # The angle sector of a compass point starts 1/2 sector range before @@ -496,7 +503,7 @@ class Compass: def l10n( self, - unit: Units = "Point", + unit: CompassUnits = "Point", locale: babel.Locale | GeoLocation | None = None, template: str = "{value}{unit}", num_pattern: str = "#,##0", @@ -514,7 +521,7 @@ class Compass: return template.format(value=val_str, unit=unit) -WeatherConditionType = typing.Literal[ +WeatherConditionType = t.Literal[ # The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel") "clear sky", "partly cloudy", @@ -632,7 +639,7 @@ YR_WEATHER_SYMBOL_MAP = { if __name__ == "__main__": # test: fetch all symbols of the type catalog .. - for c in typing.get_args(WeatherConditionType): + for c in t.get_args(WeatherConditionType): symbol_url(condition=c) _cache = get_WEATHER_DATA_CACHE() |