mirror of
				https://github.com/searxng/searxng.git
				synced 2025-10-30 18:22:31 -04:00 
			
		
		
		
	[fix] JSON format: serialization of the result-types
The ``JSONEncoder`` (``format="json"``) must perform a conversion to the
built-in types for the ``msgspec.Struct``::
    if isinstance(o, msgspec.Struct):
        return msgspec.to_builtins(o)
The result types are already of type ``msgspec.Struct``, so they can be
converted into built-in types.
The field types (in the result type) that were not yet of type ``msgspec.Struct``
have been converted to::
    searx.weather.GeoLocation@dataclass -> msgspec.Struct
    searx.weather.DateTime              -> msgspec.Struct
    searx.weather.Temperature           -> msgspec.Struct
    searx.weather.PressureUnits         -> msgspec.Struct
    searx.weather.WindSpeed             -> msgspec.Struct
    searx.weather.RelativeHumidity      -> msgspec.Struct
    searx.weather.Compass               -> msgspec.Struct
BTW: Wherever it seemed sensible, the typing was also modernized in the modified
files.
Closes: https://github.com/searxng/searxng/issues/5250
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
			
			
This commit is contained in:
		
							parent
							
								
									41e0f2abf0
								
							
						
					
					
						commit
						e16b6cb148
					
				| @ -76,12 +76,12 @@ def _weather_data(location: weather.GeoLocation, data: dict[str, t.Any]): | |||||||
| 
 | 
 | ||||||
|     return EngineResults.types.WeatherAnswer.Item( |     return EngineResults.types.WeatherAnswer.Item( | ||||||
|         location=location, |         location=location, | ||||||
|         temperature=weather.Temperature(unit="°C", value=data['temperature']), |         temperature=weather.Temperature(val=data['temperature'], unit="°C"), | ||||||
|         condition=WEATHERKIT_TO_CONDITION[data["conditionCode"]], |         condition=WEATHERKIT_TO_CONDITION[data["conditionCode"]], | ||||||
|         feels_like=weather.Temperature(unit="°C", value=data['temperatureApparent']), |         feels_like=weather.Temperature(val=data['temperatureApparent'], unit="°C"), | ||||||
|         wind_from=weather.Compass(data["windDirection"]), |         wind_from=weather.Compass(data["windDirection"]), | ||||||
|         wind_speed=weather.WindSpeed(data["windSpeed"], unit="mi/h"), |         wind_speed=weather.WindSpeed(val=data["windSpeed"], unit="mi/h"), | ||||||
|         pressure=weather.Pressure(data["pressure"], unit="hPa"), |         pressure=weather.Pressure(val=data["pressure"], unit="hPa"), | ||||||
|         humidity=weather.RelativeHumidity(data["humidity"] * 100), |         humidity=weather.RelativeHumidity(data["humidity"] * 100), | ||||||
|         cloud_cover=data["cloudCover"] * 100, |         cloud_cover=data["cloudCover"] * 100, | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| # SPDX-License-Identifier: AGPL-3.0-or-later | # SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
| """Open Meteo (weather)""" | """Open Meteo (weather)""" | ||||||
| 
 | 
 | ||||||
|  | import typing as t | ||||||
|  | 
 | ||||||
| from urllib.parse import urlencode | from urllib.parse import urlencode | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| @ -106,16 +108,16 @@ WMO_TO_CONDITION: dict[int, weather.WeatherConditionType] = { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _weather_data(location: weather.GeoLocation, data: dict): | def _weather_data(location: weather.GeoLocation, data: dict[str, t.Any]): | ||||||
| 
 | 
 | ||||||
|     return WeatherAnswer.Item( |     return WeatherAnswer.Item( | ||||||
|         location=location, |         location=location, | ||||||
|         temperature=weather.Temperature(unit="°C", value=data["temperature_2m"]), |         temperature=weather.Temperature(val=data["temperature_2m"], unit="°C"), | ||||||
|         condition=WMO_TO_CONDITION[data["weather_code"]], |         condition=WMO_TO_CONDITION[data["weather_code"]], | ||||||
|         feels_like=weather.Temperature(unit="°C", value=data["apparent_temperature"]), |         feels_like=weather.Temperature(val=data["apparent_temperature"], unit="°C"), | ||||||
|         wind_from=weather.Compass(data["wind_direction_10m"]), |         wind_from=weather.Compass(data["wind_direction_10m"]), | ||||||
|         wind_speed=weather.WindSpeed(data["wind_speed_10m"], unit="km/h"), |         wind_speed=weather.WindSpeed(val=data["wind_speed_10m"], unit="km/h"), | ||||||
|         pressure=weather.Pressure(data["pressure_msl"], unit="hPa"), |         pressure=weather.Pressure(val=data["pressure_msl"], unit="hPa"), | ||||||
|         humidity=weather.RelativeHumidity(data["relative_humidity_2m"]), |         humidity=weather.RelativeHumidity(data["relative_humidity_2m"]), | ||||||
|         cloud_cover=data["cloud_cover"], |         cloud_cover=data["cloud_cover"], | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| # SPDX-License-Identifier: AGPL-3.0-or-later | # SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
| """wttr.in (weather forecast service)""" | """wttr.in (weather forecast service)""" | ||||||
| 
 | 
 | ||||||
|  | import typing as t | ||||||
|  | 
 | ||||||
| from urllib.parse import quote | from urllib.parse import quote | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| @ -80,19 +82,19 @@ def request(query, params): | |||||||
|     return params |     return params | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _weather_data(location: weather.GeoLocation, data: dict): | def _weather_data(location: weather.GeoLocation, data: dict[str, t.Any]): | ||||||
|     # the naming between different data objects is inconsitent, thus temp_C and |     # the naming between different data objects is inconsitent, thus temp_C and | ||||||
|     # tempC are possible |     # tempC are possible | ||||||
|     tempC: float = data.get("temp_C") or data.get("tempC")  # type: ignore |     tempC: float = data.get("temp_C") or data.get("tempC")  # type: ignore | ||||||
| 
 | 
 | ||||||
|     return WeatherAnswer.Item( |     return WeatherAnswer.Item( | ||||||
|         location=location, |         location=location, | ||||||
|         temperature=weather.Temperature(unit="°C", value=tempC), |         temperature=weather.Temperature(val=tempC, unit="°C"), | ||||||
|         condition=WWO_TO_CONDITION[data["weatherCode"]], |         condition=WWO_TO_CONDITION[data["weatherCode"]], | ||||||
|         feels_like=weather.Temperature(unit="°C", value=data["FeelsLikeC"]), |         feels_like=weather.Temperature(val=data["FeelsLikeC"], unit="°C"), | ||||||
|         wind_from=weather.Compass(int(data["winddirDegree"])), |         wind_from=weather.Compass(int(data["winddirDegree"])), | ||||||
|         wind_speed=weather.WindSpeed(data["windspeedKmph"], unit="km/h"), |         wind_speed=weather.WindSpeed(val=data["windspeedKmph"], unit="km/h"), | ||||||
|         pressure=weather.Pressure(data["pressure"], unit="hPa"), |         pressure=weather.Pressure(val=data["pressure"], unit="hPa"), | ||||||
|         humidity=weather.RelativeHumidity(data["humidity"]), |         humidity=weather.RelativeHumidity(data["humidity"]), | ||||||
|         cloud_cover=data["cloudcover"], |         cloud_cover=data["cloudcover"], | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| # SPDX-License-Identifier: AGPL-3.0-or-later | # SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
| # pylint: disable=too-few-public-methods,missing-module-docstring | # pylint: disable=too-few-public-methods,missing-module-docstring | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| __all__ = ["PluginInfo", "Plugin", "PluginCfg", "PluginStorage"] | __all__ = ["PluginInfo", "Plugin", "PluginCfg", "PluginStorage"] | ||||||
| 
 | 
 | ||||||
| import abc | import abc | ||||||
| @ -9,16 +8,17 @@ import importlib | |||||||
| import inspect | import inspect | ||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| import typing | 
 | ||||||
| from collections.abc import Sequence | import typing as t | ||||||
|  | from collections.abc import Generator | ||||||
| 
 | 
 | ||||||
| from dataclasses import dataclass, field | from dataclasses import dataclass, field | ||||||
| 
 | 
 | ||||||
| from searx.extended_types import SXNG_Request | from searx.extended_types import SXNG_Request | ||||||
| from searx.result_types import Result |  | ||||||
| 
 | 
 | ||||||
| if typing.TYPE_CHECKING: | if t.TYPE_CHECKING: | ||||||
|     from searx.search import SearchWithPlugins |     from searx.search import SearchWithPlugins | ||||||
|  |     from searx.result_types import Result, EngineResults, LegacyResult  # pyright: ignore[reportPrivateLocalImportUsage] | ||||||
|     import flask |     import flask | ||||||
| 
 | 
 | ||||||
| log: logging.Logger = logging.getLogger("searx.plugins") | log: logging.Logger = logging.getLogger("searx.plugins") | ||||||
| @ -42,7 +42,7 @@ class PluginInfo: | |||||||
|     description: str |     description: str | ||||||
|     """Short description of the *answerer*.""" |     """Short description of the *answerer*.""" | ||||||
| 
 | 
 | ||||||
|     preference_section: typing.Literal["general", "ui", "privacy", "query"] | None = "general" |     preference_section: t.Literal["general", "ui", "privacy", "query"] | None = "general" | ||||||
|     """Section (tab/group) in the preferences where this plugin is shown to the |     """Section (tab/group) in the preferences where this plugin is shown to the | ||||||
|     user. |     user. | ||||||
| 
 | 
 | ||||||
| @ -71,7 +71,7 @@ class Plugin(abc.ABC): | |||||||
|     id: str = "" |     id: str = "" | ||||||
|     """The ID (suffix) in the HTML form.""" |     """The ID (suffix) in the HTML form.""" | ||||||
| 
 | 
 | ||||||
|     active: typing.ClassVar[bool] |     active: t.ClassVar[bool] | ||||||
|     """Plugin is enabled/disabled by default (:py:obj:`PluginCfg.active`).""" |     """Plugin is enabled/disabled by default (:py:obj:`PluginCfg.active`).""" | ||||||
| 
 | 
 | ||||||
|     keywords: list[str] = [] |     keywords: list[str] = [] | ||||||
| @ -109,7 +109,7 @@ class Plugin(abc.ABC): | |||||||
|             raise ValueError(f"plugin ID {self.id} contains invalid character (use lowercase ASCII)") |             raise ValueError(f"plugin ID {self.id} contains invalid character (use lowercase ASCII)") | ||||||
| 
 | 
 | ||||||
|         if not getattr(self, "log", None): |         if not getattr(self, "log", None): | ||||||
|             pkg_name = inspect.getmodule(self.__class__).__package__  # type: ignore |             pkg_name = inspect.getmodule(self.__class__).__package__  # pyright: ignore[reportOptionalMemberAccess] | ||||||
|             self.log = logging.getLogger(f"{pkg_name}.{self.id}") |             self.log = logging.getLogger(f"{pkg_name}.{self.id}") | ||||||
| 
 | 
 | ||||||
|     def __hash__(self) -> int: |     def __hash__(self) -> int: | ||||||
| @ -120,7 +120,7 @@ class Plugin(abc.ABC): | |||||||
| 
 | 
 | ||||||
|         return id(self) |         return id(self) | ||||||
| 
 | 
 | ||||||
|     def __eq__(self, other: typing.Any): |     def __eq__(self, other: t.Any): | ||||||
|         """py:obj:`Plugin` objects are equal if the hash values of the two |         """py:obj:`Plugin` objects are equal if the hash values of the two | ||||||
|         objects are equal.""" |         objects are equal.""" | ||||||
| 
 | 
 | ||||||
| @ -146,7 +146,7 @@ class Plugin(abc.ABC): | |||||||
|         """ |         """ | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool: |     def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: "Result") -> bool: | ||||||
|         """Runs for each result of each engine and returns a boolean: |         """Runs for each result of each engine and returns a boolean: | ||||||
| 
 | 
 | ||||||
|         - ``True`` to keep the result |         - ``True`` to keep the result | ||||||
| @ -166,7 +166,9 @@ class Plugin(abc.ABC): | |||||||
|         """ |         """ | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | Sequence[Result]: |     def post_search( | ||||||
|  |         self, request: SXNG_Request, search: "SearchWithPlugins" | ||||||
|  |     ) -> "None | list[Result | LegacyResult] | EngineResults": | ||||||
|         """Runs AFTER the search request.  Can return a list of |         """Runs AFTER the search request.  Can return a list of | ||||||
|         :py:obj:`Result <searx.result_types._base.Result>` objects to be added to the |         :py:obj:`Result <searx.result_types._base.Result>` objects to be added to the | ||||||
|         final result list.""" |         final result list.""" | ||||||
| @ -196,7 +198,7 @@ class PluginStorage: | |||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.plugin_list = set() |         self.plugin_list = set() | ||||||
| 
 | 
 | ||||||
|     def __iter__(self): |     def __iter__(self) -> Generator[Plugin]: | ||||||
|         yield from self.plugin_list |         yield from self.plugin_list | ||||||
| 
 | 
 | ||||||
|     def __len__(self): |     def __len__(self): | ||||||
| @ -207,7 +209,7 @@ class PluginStorage: | |||||||
| 
 | 
 | ||||||
|         return [p.info for p in self.plugin_list] |         return [p.info for p in self.plugin_list] | ||||||
| 
 | 
 | ||||||
|     def load_settings(self, cfg: dict[str, dict[str, typing.Any]]): |     def load_settings(self, cfg: dict[str, dict[str, t.Any]]): | ||||||
|         """Load plugins configured in SearXNG's settings :ref:`settings |         """Load plugins configured in SearXNG's settings :ref:`settings | ||||||
|         plugins`.""" |         plugins`.""" | ||||||
| 
 | 
 | ||||||
| @ -262,7 +264,7 @@ class PluginStorage: | |||||||
|                 break |                 break | ||||||
|         return ret |         return ret | ||||||
| 
 | 
 | ||||||
|     def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool: |     def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: "Result") -> bool: | ||||||
| 
 | 
 | ||||||
|         ret = True |         ret = True | ||||||
|         for plugin in [p for p in self.plugin_list if p.id in search.user_plugins]: |         for plugin in [p for p in self.plugin_list if p.id in search.user_plugins]: | ||||||
|  | |||||||
| @ -1,12 +1,11 @@ | |||||||
| # SPDX-License-Identifier: AGPL-3.0-or-later | # SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
| # pylint: disable=missing-module-docstring | # pylint: disable=missing-module-docstring | ||||||
| 
 | 
 | ||||||
| from __future__ import annotations |  | ||||||
| import typing as t | import typing as t | ||||||
| 
 | 
 | ||||||
| import datetime | import datetime | ||||||
| 
 | 
 | ||||||
| from flask_babel import gettext  # type: ignore | from flask_babel import gettext | ||||||
| from searx.result_types import EngineResults | from searx.result_types import EngineResults | ||||||
| from searx.weather import DateTime, GeoLocation | from searx.weather import DateTime, GeoLocation | ||||||
| 
 | 
 | ||||||
| @ -53,13 +52,13 @@ class SXNGPlugin(Plugin): | |||||||
|         search_term = " ".join(query_parts).strip() |         search_term = " ".join(query_parts).strip() | ||||||
| 
 | 
 | ||||||
|         if not search_term: |         if not search_term: | ||||||
|             date_time = DateTime(time=datetime.datetime.now()) |             date_time = DateTime(datetime.datetime.now()) | ||||||
|             results.add(results.types.Answer(answer=date_time.l10n())) |             results.add(results.types.Answer(answer=date_time.l10n())) | ||||||
|             return results |             return results | ||||||
| 
 | 
 | ||||||
|         geo = GeoLocation.by_query(search_term=search_term) |         geo = GeoLocation.by_query(search_term=search_term) | ||||||
|         if geo: |         if geo: | ||||||
|             date_time = DateTime(time=datetime.datetime.now(tz=geo.zoneinfo)) |             date_time = DateTime(datetime.datetime.now(tz=geo.zoneinfo)) | ||||||
|             tz_name = geo.timezone.replace('_', ' ') |             tz_name = geo.timezone.replace('_', ' ') | ||||||
|             results.add( |             results.add( | ||||||
|                 results.types.Answer( |                 results.types.Answer( | ||||||
|  | |||||||
							
								
								
									
										195
									
								
								searx/weather.py
									
									
									
									
									
								
							
							
						
						
									
										195
									
								
								searx/weather.py
									
									
									
									
									
								
							| @ -14,11 +14,10 @@ __all__ = [ | |||||||
|     "GeoLocation", |     "GeoLocation", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| import typing | import typing as t | ||||||
| 
 | 
 | ||||||
| import base64 | import base64 | ||||||
| import datetime | import datetime | ||||||
| import dataclasses |  | ||||||
| import zoneinfo | import zoneinfo | ||||||
| 
 | 
 | ||||||
| from urllib.parse import quote_plus | from urllib.parse import quote_plus | ||||||
| @ -27,7 +26,8 @@ import babel | |||||||
| import babel.numbers | import babel.numbers | ||||||
| import babel.dates | import babel.dates | ||||||
| import babel.languages | import babel.languages | ||||||
| import flask_babel | import flask_babel  # pyright: ignore[reportMissingTypeStubs] | ||||||
|  | import msgspec | ||||||
| 
 | 
 | ||||||
| from searx import network | from searx import network | ||||||
| from searx.cache import ExpireCache, ExpireCacheCfg | from searx.cache import ExpireCache, ExpireCacheCfg | ||||||
| @ -120,8 +120,7 @@ def symbol_url(condition: "WeatherConditionType") -> str | None: | |||||||
|     return data_url |     return data_url | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @dataclasses.dataclass | class GeoLocation(msgspec.Struct, kw_only=True): | ||||||
| class GeoLocation: |  | ||||||
|     """Minimal implementation of Geocoding.""" |     """Minimal implementation of Geocoding.""" | ||||||
| 
 | 
 | ||||||
|     # The type definition was based on the properties from the geocoding API of |     # The type definition was based on the properties from the geocoding API of | ||||||
| @ -176,6 +175,8 @@ class GeoLocation: | |||||||
| 
 | 
 | ||||||
|         ctx = "weather_geolocation_by_query" |         ctx = "weather_geolocation_by_query" | ||||||
|         cache = get_WEATHER_DATA_CACHE() |         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) |         geo_props = cache.get(search_term, ctx=ctx) | ||||||
| 
 | 
 | ||||||
|         if not geo_props: |         if not geo_props: | ||||||
| @ -194,15 +195,14 @@ class GeoLocation: | |||||||
|         if not results: |         if not results: | ||||||
|             raise ValueError(f"unknown geo location: '{search_term}'") |             raise ValueError(f"unknown geo location: '{search_term}'") | ||||||
|         location = results[0] |         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"] | DateTimeFormats = t.Literal["full", "long", "medium", "short"] | ||||||
| DateTimeLocaleTypes = typing.Literal["UI"] | DateTimeLocaleTypes = t.Literal["UI"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @typing.final | class DateTime(msgspec.Struct): | ||||||
| class DateTime: |  | ||||||
|     """Class to represent date & time.  Essentially, it is a wrapper that |     """Class to represent date & time.  Essentially, it is a wrapper that | ||||||
|     conveniently combines :py:obj:`datetime.datetime` and |     conveniently combines :py:obj:`datetime.datetime` and | ||||||
|     :py:obj:`babel.dates.format_datetime`.  A conversion of time zones is not |     :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``. |     as the value for the ``locale``. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, time: datetime.datetime): |     datetime: datetime.datetime | ||||||
|         self.datetime = time |  | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.l10n() |         return self.l10n() | ||||||
| @ -252,36 +251,35 @@ class DateTime: | |||||||
|         return babel.dates.format_date(self.datetime, format=fmt, locale=locale) |         return babel.dates.format_date(self.datetime, format=fmt, locale=locale) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @typing.final | TemperatureUnits: t.TypeAlias = t.Literal["°C", "°F", "K"] | ||||||
| class Temperature: | 
 | ||||||
|  | 
 | ||||||
|  | class Temperature(msgspec.Struct, kw_only=True): | ||||||
|     """Class for converting temperature units and for string representation of |     """Class for converting temperature units and for string representation of | ||||||
|     measured values.""" |     measured values.""" | ||||||
| 
 | 
 | ||||||
|     si_name = "Q11579" |     val: float | ||||||
|  |     unit: TemperatureUnits | ||||||
| 
 | 
 | ||||||
|     Units = typing.Literal["°C", "°F", "K"] |     si_name: t.ClassVar[str] = "Q11579" | ||||||
|     """Supported temperature units.""" |     units: t.ClassVar[list[str]] = list(t.get_args(TemperatureUnits)) | ||||||
| 
 | 
 | ||||||
|     units = list(typing.get_args(Units)) |     def __post_init__(self): | ||||||
| 
 |         if self.unit not in self.units: | ||||||
|     def __init__(self, value: float, unit: Units): |             raise ValueError(f"invalid unit: {self.unit}") | ||||||
|         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 __str__(self): |     def __str__(self): | ||||||
|         return self.l10n() |         return self.l10n() | ||||||
| 
 | 
 | ||||||
|     def value(self, unit: Units) -> float: |     def value(self, unit: TemperatureUnits) -> float: | ||||||
|         return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si) |         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( |     def l10n( | ||||||
|         self, |         self, | ||||||
|         unit: Units | None = None, |         unit: TemperatureUnits | None = None, | ||||||
|         locale: babel.Locale | GeoLocation | None = None, |         locale: babel.Locale | GeoLocation | None = None, | ||||||
|         template: str = "{value} {unit}", |         template: str = "{value} {unit}", | ||||||
|         num_pattern: str = "#,##0", |         num_pattern: str = "#,##0", | ||||||
| @ -320,33 +318,35 @@ class Temperature: | |||||||
|         return template.format(value=val_str, unit=unit) |         return template.format(value=val_str, unit=unit) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @typing.final | PressureUnits: t.TypeAlias = t.Literal["Pa", "hPa", "cm Hg", "bar"] | ||||||
| class Pressure: | 
 | ||||||
|  | 
 | ||||||
|  | class Pressure(msgspec.Struct, kw_only=True): | ||||||
|     """Class for converting pressure units and for string representation of |     """Class for converting pressure units and for string representation of | ||||||
|     measured values.""" |     measured values.""" | ||||||
| 
 | 
 | ||||||
|     si_name = "Q44395" |     val: float | ||||||
|  |     unit: PressureUnits | ||||||
| 
 | 
 | ||||||
|     Units = typing.Literal["Pa", "hPa", "cm Hg", "bar"] |     si_name: t.ClassVar[str] = "Q44395" | ||||||
|     """Supported units.""" |     units: t.ClassVar[list[str]] = list(t.get_args(PressureUnits)) | ||||||
| 
 | 
 | ||||||
|     units = list(typing.get_args(Units)) |     def __post_init__(self): | ||||||
| 
 |         if self.unit not in self.units: | ||||||
|     def __init__(self, value: float, unit: Units): |             raise ValueError(f"invalid unit: {self.unit}") | ||||||
|         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 __str__(self): |     def __str__(self): | ||||||
|         return self.l10n() |         return self.l10n() | ||||||
| 
 | 
 | ||||||
|     def value(self, unit: Units) -> float: |     def value(self, unit: PressureUnits) -> float: | ||||||
|         return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si) |         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( |     def l10n( | ||||||
|         self, |         self, | ||||||
|         unit: Units | None = None, |         unit: PressureUnits | None = None, | ||||||
|         locale: babel.Locale | GeoLocation | None = None, |         locale: babel.Locale | GeoLocation | None = None, | ||||||
|         template: str = "{value} {unit}", |         template: str = "{value} {unit}", | ||||||
|         num_pattern: str = "#,##0", |         num_pattern: str = "#,##0", | ||||||
| @ -363,8 +363,10 @@ class Pressure: | |||||||
|         return template.format(value=val_str, unit=unit) |         return template.format(value=val_str, unit=unit) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @typing.final | WindSpeedUnits: t.TypeAlias = t.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"] | ||||||
| class WindSpeed: | 
 | ||||||
|  | 
 | ||||||
|  | class WindSpeed(msgspec.Struct, kw_only=True): | ||||||
|     """Class for converting speed or velocity units and for string |     """Class for converting speed or velocity units and for string | ||||||
|     representation of measured values. |     representation of measured values. | ||||||
| 
 | 
 | ||||||
| @ -375,28 +377,28 @@ class WindSpeed: | |||||||
|        (55.6 m/s) |        (55.6 m/s) | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     si_name = "Q182429" |     val: float | ||||||
|  |     unit: WindSpeedUnits | ||||||
| 
 | 
 | ||||||
|     Units = typing.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"] |     si_name: t.ClassVar[str] = "Q182429" | ||||||
|     """Supported units.""" |     units: t.ClassVar[list[str]] = list(t.get_args(WindSpeedUnits)) | ||||||
| 
 | 
 | ||||||
|     units = list(typing.get_args(Units)) |     def __post_init__(self): | ||||||
| 
 |         if self.unit not in self.units: | ||||||
|     def __init__(self, value: float, unit: Units): |             raise ValueError(f"invalid unit: {self.unit}") | ||||||
|         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 __str__(self): |     def __str__(self): | ||||||
|         return self.l10n() |         return self.l10n() | ||||||
| 
 | 
 | ||||||
|     def value(self, unit: Units) -> float: |     def value(self, unit: WindSpeedUnits) -> float: | ||||||
|         return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si) |         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( |     def l10n( | ||||||
|         self, |         self, | ||||||
|         unit: Units | None = None, |         unit: WindSpeedUnits | None = None, | ||||||
|         locale: babel.Locale | GeoLocation | None = None, |         locale: babel.Locale | GeoLocation | None = None, | ||||||
|         template: str = "{value} {unit}", |         template: str = "{value} {unit}", | ||||||
|         num_pattern: str = "#,##0", |         num_pattern: str = "#,##0", | ||||||
| @ -413,23 +415,23 @@ class WindSpeed: | |||||||
|         return template.format(value=val_str, unit=unit) |         return template.format(value=val_str, unit=unit) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @typing.final | RelativeHumidityUnits: t.TypeAlias = t.Literal["%"] | ||||||
| class RelativeHumidity: | 
 | ||||||
|  | 
 | ||||||
|  | class RelativeHumidity(msgspec.Struct): | ||||||
|     """Amount of relative humidity in the air. The unit is ``%``""" |     """Amount of relative humidity in the air. The unit is ``%``""" | ||||||
| 
 | 
 | ||||||
|     Units = typing.Literal["%"] |     val: float | ||||||
|     """Supported unit.""" |  | ||||||
| 
 | 
 | ||||||
|     units = list(typing.get_args(Units)) |     # there exists only one unit (%) --> set "%" as the final value (constant) | ||||||
| 
 |     unit: t.ClassVar["t.Final[RelativeHumidityUnits]"] = "%" | ||||||
|     def __init__(self, humidity: float): |     units: t.ClassVar[list[str]] = list(t.get_args(RelativeHumidityUnits)) | ||||||
|         self.humidity = humidity |  | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.l10n() |         return self.l10n() | ||||||
| 
 | 
 | ||||||
|     def value(self) -> float: |     def value(self) -> float: | ||||||
|         return self.humidity |         return self.val | ||||||
| 
 | 
 | ||||||
|     def l10n( |     def l10n( | ||||||
|         self, |         self, | ||||||
| @ -447,45 +449,50 @@ class RelativeHumidity: | |||||||
|         return template.format(value=val_str, unit=unit) |         return template.format(value=val_str, unit=unit) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @typing.final | CompassPoint: t.TypeAlias = t.Literal[ | ||||||
| class Compass: |     "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" | ||||||
|  | ] | ||||||
|  | """Compass point type definition""" | ||||||
|  | 
 | ||||||
|  | CompassUnits: t.TypeAlias = t.Literal["°", "Point"] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Compass(msgspec.Struct): | ||||||
|     """Class for converting compass points and azimuth values (360°)""" |     """Class for converting compass points and azimuth values (360°)""" | ||||||
| 
 | 
 | ||||||
|     Units = typing.Literal["°", "Point"] |     val: "float | int | CompassPoint" | ||||||
|  |     unit: CompassUnits = "°" | ||||||
| 
 | 
 | ||||||
|     Point = typing.Literal[ |     TURN: t.ClassVar[float] = 360.0 | ||||||
|         "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" |  | ||||||
|     ] |  | ||||||
|     """Compass point type definition""" |  | ||||||
| 
 |  | ||||||
|     TURN = 360.0 |  | ||||||
|     """Full turn (360°)""" |     """Full turn (360°)""" | ||||||
| 
 | 
 | ||||||
|     POINTS = list(typing.get_args(Point)) |     POINTS: t.ClassVar[list[CompassPoint]] = list(t.get_args(CompassPoint)) | ||||||
|     """Compass points.""" |     """Compass points.""" | ||||||
| 
 | 
 | ||||||
|     RANGE = TURN / len(POINTS) |     RANGE: t.ClassVar[float] = TURN / len(POINTS) | ||||||
|     """Angle sector of a compass point""" |     """Angle sector of a compass point""" | ||||||
| 
 | 
 | ||||||
|     def __init__(self, azimuth: float | int | Point): |     def __post_init__(self): | ||||||
|         if isinstance(azimuth, str): |         if isinstance(self.val, str): | ||||||
|             if azimuth not in self.POINTS: |             if self.val not in self.POINTS: | ||||||
|                 raise ValueError(f"Invalid compass point: {azimuth}") |                 raise ValueError(f"Invalid compass point: {self.val}") | ||||||
|             azimuth = self.POINTS.index(azimuth) * self.RANGE |             self.val = self.POINTS.index(self.val) * self.RANGE | ||||||
|         self.azimuth = azimuth % self.TURN | 
 | ||||||
|  |         self.val = self.val % self.TURN | ||||||
|  |         self.unit = "°" | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.l10n() |         return self.l10n() | ||||||
| 
 | 
 | ||||||
|     def value(self, unit: Units): |     def value(self, unit: CompassUnits): | ||||||
|         if unit == "Point": |         if unit == "Point" and isinstance(self.val, float): | ||||||
|             return self.point(self.azimuth) |             return self.point(self.val) | ||||||
|         if unit == "°": |         if unit == "°": | ||||||
|             return self.azimuth |             return self.val | ||||||
|         raise ValueError(f"unknown unit: {unit}") |         raise ValueError(f"unknown unit: {unit}") | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def point(cls, azimuth: float | int) -> Point: |     def point(cls, azimuth: float | int) -> CompassPoint: | ||||||
|         """Returns the compass point to an azimuth value.""" |         """Returns the compass point to an azimuth value.""" | ||||||
|         azimuth = azimuth % cls.TURN |         azimuth = azimuth % cls.TURN | ||||||
|         # The angle sector of a compass point starts 1/2 sector range before |         # The angle sector of a compass point starts 1/2 sector range before | ||||||
| @ -496,7 +503,7 @@ class Compass: | |||||||
| 
 | 
 | ||||||
|     def l10n( |     def l10n( | ||||||
|         self, |         self, | ||||||
|         unit: Units = "Point", |         unit: CompassUnits = "Point", | ||||||
|         locale: babel.Locale | GeoLocation | None = None, |         locale: babel.Locale | GeoLocation | None = None, | ||||||
|         template: str = "{value}{unit}", |         template: str = "{value}{unit}", | ||||||
|         num_pattern: str = "#,##0", |         num_pattern: str = "#,##0", | ||||||
| @ -514,7 +521,7 @@ class Compass: | |||||||
|         return template.format(value=val_str, unit=unit) |         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") |     # The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel") | ||||||
|     "clear sky", |     "clear sky", | ||||||
|     "partly cloudy", |     "partly cloudy", | ||||||
| @ -632,7 +639,7 @@ YR_WEATHER_SYMBOL_MAP = { | |||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
| 
 | 
 | ||||||
|     # test: fetch all symbols of the type catalog .. |     # 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) |         symbol_url(condition=c) | ||||||
| 
 | 
 | ||||||
|     _cache = get_WEATHER_DATA_CACHE() |     _cache = get_WEATHER_DATA_CACHE() | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ from typing import Iterable, List, Tuple, TYPE_CHECKING | |||||||
| from io import StringIO | from io import StringIO | ||||||
| from codecs import getincrementalencoder | from codecs import getincrementalencoder | ||||||
| 
 | 
 | ||||||
|  | import msgspec | ||||||
| from flask_babel import gettext, format_date  # type: ignore | from flask_babel import gettext, format_date  # type: ignore | ||||||
| 
 | 
 | ||||||
| from searx import logger, get_setting | from searx import logger, get_setting | ||||||
| @ -147,6 +148,8 @@ def write_csv_response(csv: CSVWriter, rc: "ResultContainer") -> None:  # pylint | |||||||
| 
 | 
 | ||||||
| class JSONEncoder(json.JSONEncoder):  # pylint: disable=missing-class-docstring | class JSONEncoder(json.JSONEncoder):  # pylint: disable=missing-class-docstring | ||||||
|     def default(self, o): |     def default(self, o): | ||||||
|  |         if isinstance(o, msgspec.Struct): | ||||||
|  |             return msgspec.to_builtins(o) | ||||||
|         if isinstance(o, datetime): |         if isinstance(o, datetime): | ||||||
|             return o.isoformat() |             return o.isoformat() | ||||||
|         if isinstance(o, timedelta): |         if isinstance(o, timedelta): | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user