mirror of
https://github.com/searxng/searxng.git
synced 2025-12-17 02:25:08 -05: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