2"""Implementations used for weather conditions and forecast."""
12 "WeatherConditionType",
24from urllib.parse
import quote_plus
32from searx
import network
37WEATHER_DATA_CACHE: ExpireCache |
None =
None
38"""A simple cache for weather data (geo-locations, icons, ..)"""
40YR_WEATHER_SYMBOL_URL =
"https://raw.githubusercontent.com/nrkno/yr-weather-symbols/refs/heads/master/symbols/outline"
45 global WEATHER_DATA_CACHE
47 if WEATHER_DATA_CACHE
is None:
48 WEATHER_DATA_CACHE = ExpireCache.build_cache(
50 name=
"WEATHER_DATA_CACHE",
51 MAX_VALUE_LEN=1024 * 200,
52 MAXHOLD_TIME=60 * 60 * 24 * 7 * 4,
55 return WEATHER_DATA_CACHE
75 from searx
import query
79 if query.languages
and query.languages[0]
not in [
"all",
"auto"]:
80 return query.languages[0]
82 search_lang = sxng_request.form.get(
"language")
83 if search_lang
and search_lang
not in [
"all",
"auto"]:
86 client_pref = ClientPref.from_http_request(sxng_request)
87 search_lang = client_pref.locale_tag
88 if search_lang
and search_lang
not in [
"all",
"auto"]:
93def symbol_url(condition:
"WeatherConditionType") -> str |
None:
94 """Returns ``data:`` URL for the weather condition symbol or ``None`` if
95 the condition is not of type :py:obj:`WeatherConditionType`.
97 If symbol (SVG) is not already in the :py:obj:`WEATHER_DATA_CACHE` its
98 fetched from https://github.com/nrkno/yr-weather-symbols
103 fname = YR_WEATHER_SYMBOL_MAP.get(condition)
107 ctx =
"weather_symbol_url"
109 origin_url = f
"{YR_WEATHER_SYMBOL_URL}/{fname}.svg"
111 data_url = cache.get(origin_url, ctx=ctx)
112 if data_url
is not None:
115 response = network.get(origin_url, timeout=3)
116 if response.status_code == 200:
117 mimetype = response.headers[
'Content-Type']
118 data_url = f
"data:{mimetype};base64,{str(base64.b64encode(response.content), 'utf-8')}"
119 cache.set(key=origin_url, value=data_url, expire=
None, ctx=ctx)
123@dataclasses.dataclass
125 """Minimal implementation of Geocoding."""
159 locale = babel.Locale.parse(f
"{lang}_{self.country_code}")
161 except babel.UnknownLocaleError:
168 return babel.Locale(
"en", territory=
"DE")
171 def by_query(cls, search_term: str) ->
"GeoLocation":
172 """Factory method to get a GeoLocation object by a search term. If no
173 location can be determined for the search term, a :py:obj:`ValueError`
177 ctx =
"weather_geolocation_by_query"
179 geo_props = cache.get(search_term, ctx=ctx)
183 cache.set(key=search_term, value=geo_props, expire=
None, ctx=ctx)
185 return cls(**geo_props)
189 url = f
"https://geocoding-api.open-meteo.com/v1/search?name={quote_plus(search_term)}"
190 resp = network.get(url, timeout=3)
191 if resp.status_code != 200:
192 raise ValueError(f
"unknown geo location: '{search_term}'")
193 results = resp.json().get(
"results")
195 raise ValueError(f
"unknown geo location: '{search_term}'")
196 location = results[0]
197 return {field.name: location[field.name]
for field
in dataclasses.fields(cls)}
200DateTimeFormats = typing.Literal[
"full",
"long",
"medium",
"short"]
201DateTimeLocaleTypes = typing.Literal[
"UI"]
206 """Class to represent date & time. Essentially, it is a wrapper that
207 conveniently combines :py:obj:`datetime.datetime` and
208 :py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
209 provided (in the current version).
211 The localized string representation can be obtained via the
212 :py:obj:`DateTime.l10n` and :py:obj:`DateTime.l10n_date` methods, where the
213 ``locale`` parameter defaults to the search language. Alternatively, a
214 :py:obj:`GeoLocation` or a :py:obj:`babel.Locale` instance can be passed
215 directly. If the UI language is to be used, the string ``UI`` can be passed
216 as the value for the ``locale``.
227 fmt: DateTimeFormats | str =
"medium",
228 locale: DateTimeLocaleTypes | babel.Locale | GeoLocation |
None =
None,
230 """Localized representation of date & time."""
231 if isinstance(locale, str)
and locale ==
"UI":
232 locale = flask_babel.get_locale()
233 elif isinstance(locale, GeoLocation):
234 locale = locale.locale()
237 return babel.dates.format_datetime(self.
datetime, format=fmt, locale=locale)
241 fmt: DateTimeFormats | str =
"medium",
242 locale: DateTimeLocaleTypes | babel.Locale | GeoLocation |
None =
None,
244 """Localized representation of date."""
246 if isinstance(locale, str)
and locale ==
"UI":
247 locale = flask_babel.get_locale()
248 elif isinstance(locale, GeoLocation):
249 locale = locale.locale()
252 return babel.dates.format_date(self.
datetime, format=fmt, locale=locale)
257 """Class for converting temperature units and for string representation of
262 Units = typing.Literal[
"°C",
"°F",
"K"]
263 """Supported temperature units."""
265 units = list(typing.get_args(Units))
268 if unit
not in self.
units:
269 raise ValueError(f
"invalid unit: {unit}")
270 self.
si: float = convert_to_si(
279 def value(self, unit: Units) -> float:
280 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
284 unit: Units |
None =
None,
285 locale: babel.Locale | GeoLocation |
None =
None,
286 template: str =
"{value} {unit}",
287 num_pattern: str =
"#,##0",
289 """Localized representation of a measured value.
291 If the ``unit`` is not set, an attempt is made to determine a ``unit``
292 matching the territory of the ``locale``. If the locale is not set, an
293 attempt is made to determine it from the HTTP request.
295 The value is converted into the respective unit before formatting.
297 The argument ``num_pattern`` is used to determine the string formatting
298 of the numerical value:
300 - https://babel.pocoo.org/en/latest/numbers.html#pattern-syntax
301 - https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns
303 The argument ``template`` specifies how the **string formatted** value
304 and unit are to be arranged.
306 - `Format Specification Mini-Language
307 <https://docs.python.org/3/library/string.html#format-specification-mini-language>`.
310 if isinstance(locale, GeoLocation):
311 locale = locale.locale()
317 if locale.territory
in [
"US"]:
319 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
320 return template.format(value=val_str, unit=unit)
325 """Class for converting pressure units and for string representation of
330 Units = typing.Literal[
"Pa",
"hPa",
"cm Hg",
"bar"]
331 """Supported units."""
333 units = list(typing.get_args(Units))
336 if unit
not in self.
units:
337 raise ValueError(f
"invalid unit: {unit}")
339 self.
si: float = convert_to_si(si_name=self.
si_name, symbol=unit, value=value)
344 def value(self, unit: Units) -> float:
345 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
349 unit: Units |
None =
None,
350 locale: babel.Locale | GeoLocation |
None =
None,
351 template: str =
"{value} {unit}",
352 num_pattern: str =
"#,##0",
354 if isinstance(locale, GeoLocation):
355 locale = locale.locale()
362 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
363 return template.format(value=val_str, unit=unit)
368 """Class for converting speed or velocity units and for string
369 representation of measured values.
373 Working with unit ``Bft`` (:py:obj:`searx.wikidata_units.Beaufort`) will
374 throw a :py:obj:`ValueError` for egative values or values greater 16 Bft
380 Units = typing.Literal[
"m/s",
"km/h",
"kn",
"mph",
"mi/h",
"Bft"]
381 """Supported units."""
383 units = list(typing.get_args(Units))
386 if unit
not in self.
units:
387 raise ValueError(f
"invalid unit: {unit}")
389 self.
si: float = convert_to_si(si_name=self.
si_name, symbol=unit, value=value)
394 def value(self, unit: Units) -> float:
395 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
399 unit: Units |
None =
None,
400 locale: babel.Locale | GeoLocation |
None =
None,
401 template: str =
"{value} {unit}",
402 num_pattern: str =
"#,##0",
404 if isinstance(locale, GeoLocation):
405 locale = locale.locale()
412 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
413 return template.format(value=val_str, unit=unit)
418 """Amount of relative humidity in the air. The unit is ``%``"""
420 Units = typing.Literal[
"%"]
421 """Supported unit."""
423 units = list(typing.get_args(Units))
436 locale: babel.Locale | GeoLocation |
None =
None,
437 template: str =
"{value}{unit}",
438 num_pattern: str =
"#,##0",
440 if isinstance(locale, GeoLocation):
441 locale = locale.locale()
446 val_str = babel.numbers.format_decimal(self.
value(), locale=locale, format=num_pattern)
447 return template.format(value=val_str, unit=unit)
452 """Class for converting compass points and azimuth values (360°)"""
454 Units = typing.Literal[
"°",
"Point"]
456 Point = typing.Literal[
457 "N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW"
459 """Compass point type definition"""
462 """Full turn (360°)"""
464 POINTS = list(typing.get_args(Point))
465 """Compass points."""
467 RANGE = TURN / len(POINTS)
468 """Angle sector of a compass point"""
471 if isinstance(azimuth, str):
472 if azimuth
not in self.
POINTS:
473 raise ValueError(f
"Invalid compass point: {azimuth}")
485 raise ValueError(f
"unknown unit: {unit}")
488 def point(cls, azimuth: float | int) -> Point:
489 """Returns the compass point to an azimuth value."""
490 azimuth = azimuth % cls.
TURN
493 azimuth = azimuth - cls.
RANGE / 2
494 idx = int(azimuth // cls.
RANGE)
499 unit: Units =
"Point",
500 locale: babel.Locale | GeoLocation |
None =
None,
501 template: str =
"{value}{unit}",
502 num_pattern: str =
"#,##0",
504 if isinstance(locale, GeoLocation):
505 locale = locale.locale()
510 val_str = self.
value(unit)
511 return template.format(value=val_str, unit=
"")
513 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
514 return template.format(value=val_str, unit=unit)
517WeatherConditionType = typing.Literal[
525 "light rain and thunder",
526 "light rain showers and thunder",
527 "light rain showers",
530 "rain showers and thunder",
533 "heavy rain and thunder",
534 "heavy rain showers and thunder",
535 "heavy rain showers",
538 "light sleet and thunder",
539 "light sleet showers and thunder",
540 "light sleet showers",
543 "sleet showers and thunder",
546 "heavy sleet and thunder",
547 "heavy sleet showers and thunder",
548 "heavy sleet showers",
551 "light snow and thunder",
552 "light snow showers and thunder",
553 "light snow showers",
556 "snow showers and thunder",
559 "heavy snow and thunder",
560 "heavy snow showers and thunder",
561 "heavy snow showers",
564"""Standardized designations for weather conditions. The designators were
565taken from a collaboration between NRK and Norwegian Meteorological Institute
566(yr.no_). `Weather symbols`_ can be assigned to the identifiers
567(weathericons_) and they are included in the translation (i18n/l10n
568:origin:`searx/searxng.msg`).
570.. _yr.no: https://www.yr.no/en
571.. _Weather symbols: https://github.com/nrkno/yr-weather-symbols
572.. _weathericons: https://github.com/metno/weathericons
575YR_WEATHER_SYMBOL_MAP = {
577 "partly cloudy":
"03d",
582 "light rain and thunder":
"30",
583 "light rain showers and thunder":
"24d",
584 "light rain showers":
"40d",
586 "rain and thunder":
"22",
587 "rain showers and thunder":
"06d",
588 "rain showers":
"05d",
590 "heavy rain and thunder":
"11",
591 "heavy rain showers and thunder":
"25d",
592 "heavy rain showers":
"41d",
595 "light sleet and thunder":
"31",
596 "light sleet showers and thunder":
"26d",
597 "light sleet showers":
"42d",
599 "sleet and thunder":
"23",
600 "sleet showers and thunder":
"20d",
601 "sleet showers":
"07d",
603 "heavy sleet and thunder":
"32",
604 "heavy sleet showers and thunder":
"27d",
605 "heavy sleet showers":
"43d",
608 "light snow and thunder":
"33",
609 "light snow showers and thunder":
"28d",
610 "light snow showers":
"44d",
612 "snow and thunder":
"14",
613 "snow showers and thunder":
"21d",
614 "snow showers":
"08d",
616 "heavy snow and thunder":
"34",
617 "heavy snow showers and thunder":
"29d",
618 "heavy snow showers":
"45d",
621"""Map a :py:obj:`WeatherConditionType` to a `YR weather symbol`_
625 base_url = "https://raw.githubusercontent.com/nrkno/yr-weather-symbols/refs/heads/master/symbols"
626 icon_url = f"{base_url}/outline/{YR_WEATHER_SYMBOL_MAP['sleet showers']}.svg"
628.. _YR weather symbol: https://github.com/nrkno/yr-weather-symbols/blob/master/locales/en.json
632if __name__ ==
"__main__":
635 for c
in typing.get_args(WeatherConditionType):
639 title =
"cached weather condition symbols"
641 print(
"=" * len(title))
642 print(_cache.state().report())
644 title = f
"properties of {_cache.cfg.name}"
646 print(
"=" * len(title))
647 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_date(self, DateTimeFormats|str fmt="medium", DateTimeLocaleTypes|babel.Locale|GeoLocation|None locale=None)
str l10n(self, DateTimeFormats|str fmt="medium", DateTimeLocaleTypes|babel.Locale|GeoLocation|None locale=None)
__init__(self, datetime.datetime time)
"GeoLocation" by_query(cls, str search_term)
dict[str, str] _query_open_meteo(cls, str search_term)
babel.Locale locale(self)
zoneinfo.ZoneInfo zoneinfo(self)
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 _get_sxng_locale_tag()
str|None symbol_url("WeatherConditionType" condition)