2"""Implementations used for weather conditions and forecast."""
4from __future__
import annotations
13 "WeatherConditionType",
24from urllib.parse
import quote_plus
31from searx
import network
36WEATHER_DATA_CACHE: ExpireCache =
None
37"""A simple cache for weather data (geo-locations, icons, ..)"""
39YR_WEATHER_SYMBOL_URL =
"https://raw.githubusercontent.com/nrkno/yr-weather-symbols/refs/heads/master/symbols/outline"
44 global WEATHER_DATA_CACHE
46 if WEATHER_DATA_CACHE
is None:
47 WEATHER_DATA_CACHE = ExpireCache.build_cache(
49 name=
"WEATHER_DATA_CACHE",
50 MAX_VALUE_LEN=1024 * 200,
51 MAXHOLD_TIME=60 * 60 * 24 * 7 * 4,
54 return WEATHER_DATA_CACHE
74 from searx
import query
78 if query.languages
and query.languages[0]
not in [
"all",
"auto"]:
79 return query.languages[0]
81 search_lang = sxng_request.form.get(
"language")
82 if search_lang
and search_lang
not in [
"all",
"auto"]:
85 client_pref = ClientPref.from_http_request(sxng_request)
86 search_lang = client_pref.locale_tag
87 if search_lang
and search_lang
not in [
"all",
"auto"]:
92def symbol_url(condition: WeatherConditionType) -> str |
None:
93 """Returns ``data:`` URL for the weather condition symbol or ``None`` if
94 the condition is not of type :py:obj:`WeatherConditionType`.
96 If symbol (SVG) is not already in the :py:obj:`WEATHER_DATA_CACHE` its
97 fetched from https://github.com/nrkno/yr-weather-symbols
102 fname = YR_WEATHER_SYMBOL_MAP.get(condition)
106 ctx =
"weather_symbol_url"
108 origin_url = f
"{YR_WEATHER_SYMBOL_URL}/{fname}.svg"
110 data_url = cache.get(origin_url, ctx=ctx)
111 if data_url
is not None:
114 response = network.get(origin_url, timeout=3)
115 if response.status_code == 200:
116 mimetype = response.headers[
'Content-Type']
117 data_url = f
"data:{mimetype};base64,{str(base64.b64encode(response.content), 'utf-8')}"
118 cache.set(key=origin_url, value=data_url, expire=
None, ctx=ctx)
122@dataclasses.dataclass
124 """Minimal implementation of Geocoding."""
154 locale = babel.Locale.parse(f
"{lang}_{self.country_code}")
156 except babel.UnknownLocaleError:
163 return babel.Locale(
"en", territory=
"DE")
166 def by_query(cls, search_term: str) -> GeoLocation:
167 """Factory method to get a GeoLocation object by a search term. If no
168 location can be determined for the search term, a :py:obj:`ValueError`
172 ctx =
"weather_geolocation_by_query"
174 geo_props = cache.get(search_term, ctx=ctx)
178 cache.set(key=search_term, value=geo_props, expire=
None, ctx=ctx)
180 return cls(**geo_props)
184 url = f
"https://geocoding-api.open-meteo.com/v1/search?name={quote_plus(search_term)}"
185 resp = network.get(url, timeout=3)
186 if resp.status_code != 200:
187 raise ValueError(f
"unknown geo location: '{search_term}'")
188 results = resp.json().get(
"results")
190 raise ValueError(f
"unknown geo location: '{search_term}'")
191 location = results[0]
192 return {field.name: location[field.name]
for field
in dataclasses.fields(cls)}
195DateTimeFormats = typing.Literal[
"full",
"long",
"medium",
"short"]
199 """Class to represent date & time. Essentially, it is a wrapper that
200 conveniently combines :py:obj:`datetime.datetime` and
201 :py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
202 provided (in the current version).
213 fmt: DateTimeFormats | str =
"medium",
214 locale: babel.Locale | GeoLocation |
None =
None,
216 """Localized representation of date & time."""
217 if isinstance(locale, GeoLocation):
218 locale = locale.locale()
221 return babel.dates.format_datetime(self.
datetime, format=fmt, locale=locale)
225 """Class for converting temperature units and for string representation of
230 Units = typing.Literal[
"°C",
"°F",
"K"]
231 """Supported temperature units."""
233 units = list(typing.get_args(Units))
236 if unit
not in self.
units:
237 raise ValueError(f
"invalid unit: {unit}")
238 self.
si: float = convert_to_si(
247 def value(self, unit: Units) -> float:
248 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
252 unit: Units |
None =
None,
253 locale: babel.Locale | GeoLocation |
None =
None,
254 template: str =
"{value} {unit}",
255 num_pattern: str =
"#,##0",
257 """Localized representation of a measured value.
259 If the ``unit`` is not set, an attempt is made to determine a ``unit``
260 matching the territory of the ``locale``. If the locale is not set, an
261 attempt is made to determine it from the HTTP request.
263 The value is converted into the respective unit before formatting.
265 The argument ``num_pattern`` is used to determine the string formatting
266 of the numerical value:
268 - https://babel.pocoo.org/en/latest/numbers.html#pattern-syntax
269 - https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns
271 The argument ``template`` specifies how the **string formatted** value
272 and unit are to be arranged.
274 - `Format Specification Mini-Language
275 <https://docs.python.org/3/library/string.html#format-specification-mini-language>`.
278 if isinstance(locale, GeoLocation):
279 locale = locale.locale()
285 if locale.territory
in [
"US"]:
287 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
288 return template.format(value=val_str, unit=unit)
292 """Class for converting pressure units and for string representation of
297 Units = typing.Literal[
"Pa",
"hPa",
"cm Hg",
"bar"]
298 """Supported units."""
300 units = list(typing.get_args(Units))
303 if unit
not in self.
units:
304 raise ValueError(f
"invalid unit: {unit}")
306 self.
si: float = convert_to_si(si_name=self.
si_name, symbol=unit, value=value)
311 def value(self, unit: Units) -> float:
312 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
316 unit: Units |
None =
None,
317 locale: babel.Locale | GeoLocation |
None =
None,
318 template: str =
"{value} {unit}",
319 num_pattern: str =
"#,##0",
321 if isinstance(locale, GeoLocation):
322 locale = locale.locale()
329 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
330 return template.format(value=val_str, unit=unit)
334 """Class for converting speed or velocity units and for string
335 representation of measured values.
339 Working with unit ``Bft`` (:py:obj:`searx.wikidata_units.Beaufort`) will
340 throw a :py:obj:`ValueError` for egative values or values greater 16 Bft
346 Units = typing.Literal[
"m/s",
"km/h",
"kn",
"mph",
"mi/h",
"Bft"]
347 """Supported units."""
349 units = list(typing.get_args(Units))
352 if unit
not in self.
units:
353 raise ValueError(f
"invalid unit: {unit}")
355 self.
si: float = convert_to_si(si_name=self.
si_name, symbol=unit, value=value)
360 def value(self, unit: Units) -> float:
361 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
365 unit: Units |
None =
None,
366 locale: babel.Locale | GeoLocation |
None =
None,
367 template: str =
"{value} {unit}",
368 num_pattern: str =
"#,##0",
370 if isinstance(locale, GeoLocation):
371 locale = locale.locale()
378 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
379 return template.format(value=val_str, unit=unit)
383 """Amount of relative humidity in the air. The unit is ``%``"""
385 Units = typing.Literal[
"%"]
386 """Supported unit."""
388 units = list(typing.get_args(Units))
401 locale: babel.Locale | GeoLocation |
None =
None,
402 template: str =
"{value}{unit}",
403 num_pattern: str =
"#,##0",
405 if isinstance(locale, GeoLocation):
406 locale = locale.locale()
411 val_str = babel.numbers.format_decimal(self.
value(), locale=locale, format=num_pattern)
412 return template.format(value=val_str, unit=unit)
416 """Class for converting compass points and azimuth values (360°)"""
418 Units = typing.Literal[
"°",
"Point"]
420 Point = typing.Literal[
421 "N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW"
423 """Compass point type definition"""
426 """Full turn (360°)"""
428 POINTS = list(typing.get_args(Point))
429 """Compass points."""
431 RANGE = TURN / len(POINTS)
432 """Angle sector of a compass point"""
435 if isinstance(azimuth, str):
436 if azimuth
not in self.
POINTS:
437 raise ValueError(f
"Invalid compass point: {azimuth}")
449 raise ValueError(f
"unknown unit: {unit}")
452 def point(cls, azimuth: float | int) -> Point:
453 """Returns the compass point to an azimuth value."""
454 azimuth = azimuth % cls.
TURN
457 azimuth = azimuth - cls.
RANGE / 2
458 idx = int(azimuth // cls.
RANGE)
463 unit: Units =
"Point",
464 locale: babel.Locale | GeoLocation |
None =
None,
465 template: str =
"{value}{unit}",
466 num_pattern: str =
"#,##0",
468 if isinstance(locale, GeoLocation):
469 locale = locale.locale()
474 val_str = self.
value(unit)
475 return template.format(value=val_str, unit=
"")
477 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
478 return template.format(value=val_str, unit=unit)
481WeatherConditionType = typing.Literal[
487 "heavy rain and thunder",
488 "heavy rain showers and thunder",
489 "heavy rain showers",
491 "heavy sleet and thunder",
492 "heavy sleet showers and thunder",
493 "heavy sleet showers",
495 "heavy snow and thunder",
496 "heavy snow showers and thunder",
497 "heavy snow showers",
499 "light rain and thunder",
500 "light rain showers and thunder",
501 "light rain showers",
503 "light sleet and thunder",
504 "light sleet showers and thunder",
505 "light sleet showers",
507 "light snow and thunder",
508 "light snow showers and thunder",
509 "light snow showers",
513 "rain showers and thunder",
517 "sleet showers and thunder",
521 "snow showers and thunder",
525"""Standardized designations for weather conditions. The designators were
526taken from a collaboration between NRK and Norwegian Meteorological Institute
527(yr.no_). `Weather symbols`_ can be assigned to the identifiers
528(weathericons_) and they are included in the translation (i18n/l10n
529:origin:`searx/searxng.msg`).
531.. _yr.no: https://www.yr.no/en
532.. _Weather symbols: https://github.com/nrkno/yr-weather-symbols
533.. _weathericons: https://github.com/metno/weathericons
536YR_WEATHER_SYMBOL_MAP = {
539 "partly cloudy":
"03d",
541 "light rain showers":
"40d",
542 "rain showers":
"05d",
543 "heavy rain showers":
"41d",
544 "light rain showers and thunder":
"24d",
545 "rain showers and thunder":
"06d",
546 "heavy rain showers and thunder":
"25d",
547 "light sleet showers":
"42d",
548 "sleet showers":
"07d",
549 "heavy sleet showers":
"43d",
550 "light sleet showers and thunder":
"26d",
551 "sleet showers and thunder":
"20d",
552 "heavy sleet showers and thunder":
"27d",
553 "light snow showers":
"44d",
554 "snow showers":
"08d",
555 "heavy snow showers":
"45d",
556 "light snow showers and thunder":
"28d",
557 "snow showers and thunder":
"21d",
558 "heavy snow showers and thunder":
"29d",
562 "light rain and thunder":
"30",
563 "rain and thunder":
"22",
564 "heavy rain and thunder":
"11",
568 "light sleet and thunder":
"31",
569 "sleet and thunder":
"23",
570 "heavy sleet and thunder":
"32",
574 "light snow and thunder":
"33",
575 "snow and thunder":
"14",
576 "heavy snow and thunder":
"34",
579"""Map a :py:obj:`WeatherConditionType` to a `YR weather symbol`_
583 base_url = "https://raw.githubusercontent.com/nrkno/yr-weather-symbols/refs/heads/master/symbols"
584 icon_url = f"{base_url}/outline/{YR_WEATHER_SYMBOL_MAP['sleet showers']}.svg"
586.. _YR weather symbol: https://github.com/nrkno/yr-weather-symbols/blob/master/locales/en.json
590if __name__ ==
"__main__":
593 for c
in typing.get_args(WeatherConditionType):
597 title =
"cached weather condition symbols"
599 print(
"=" * len(title))
600 print(_cache.state().report())
602 title = f
"properties of {_cache.cfg.name}"
604 print(
"=" * len(title))
605 print(str(_cache.properties))
__init__(self, float|int|Point azimuth)
str l10n(self, Units unit="Point", babel.Locale|GeoLocation|None locale=None, str template="{value}{unit}", str num_pattern="#,##0")
Point point(cls, float|int azimuth)
str l10n(self, DateTimeFormats|str fmt="medium", babel.Locale|GeoLocation|None locale=None)
__init__(self, datetime.datetime time)
babel.Locale locale(self)
dict _query_open_meteo(cls, str search_term)
GeoLocation by_query(cls, str search_term)
str l10n(self, Units|None unit=None, babel.Locale|GeoLocation|None locale=None, str template="{value} {unit}", str num_pattern="#,##0")
float value(self, Units unit)
__init__(self, float value, Units unit)
__init__(self, float humidity)
str l10n(self, babel.Locale|GeoLocation|None locale=None, str template="{value}{unit}", str num_pattern="#,##0")
str l10n(self, Units|None unit=None, babel.Locale|GeoLocation|None locale=None, str template="{value} {unit}", str num_pattern="#,##0")
__init__(self, float value, Units unit)
float value(self, Units unit)
str l10n(self, Units|None unit=None, babel.Locale|GeoLocation|None locale=None, str template="{value} {unit}", str num_pattern="#,##0")
__init__(self, float value, Units unit)
float value(self, Units unit)
str|None symbol_url(WeatherConditionType condition)
str _get_sxng_locale_tag()