2"""Implementations used for weather conditions and forecast."""
12 "WeatherConditionType",
24from urllib.parse
import quote_plus
31from searx
import network
36WEATHER_DATA_CACHE: ExpireCache |
None =
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."""
158 locale = babel.Locale.parse(f
"{lang}_{self.country_code}")
160 except babel.UnknownLocaleError:
167 return babel.Locale(
"en", territory=
"DE")
170 def by_query(cls, search_term: str) ->
"GeoLocation":
171 """Factory method to get a GeoLocation object by a search term. If no
172 location can be determined for the search term, a :py:obj:`ValueError`
176 ctx =
"weather_geolocation_by_query"
178 geo_props = cache.get(search_term, ctx=ctx)
182 cache.set(key=search_term, value=geo_props, expire=
None, ctx=ctx)
184 return cls(**geo_props)
188 url = f
"https://geocoding-api.open-meteo.com/v1/search?name={quote_plus(search_term)}"
189 resp = network.get(url, timeout=3)
190 if resp.status_code != 200:
191 raise ValueError(f
"unknown geo location: '{search_term}'")
192 results = resp.json().get(
"results")
194 raise ValueError(f
"unknown geo location: '{search_term}'")
195 location = results[0]
196 return {field.name: location[field.name]
for field
in dataclasses.fields(cls)}
199DateTimeFormats = typing.Literal[
"full",
"long",
"medium",
"short"]
204 """Class to represent date & time. Essentially, it is a wrapper that
205 conveniently combines :py:obj:`datetime.datetime` and
206 :py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
207 provided (in the current version).
218 fmt: DateTimeFormats | str =
"medium",
219 locale: babel.Locale | GeoLocation |
None =
None,
221 """Localized representation of date & time."""
222 if isinstance(locale, GeoLocation):
223 locale = locale.locale()
226 return babel.dates.format_datetime(self.
datetime, format=fmt, locale=locale)
231 """Class for converting temperature units and for string representation of
236 Units = typing.Literal[
"°C",
"°F",
"K"]
237 """Supported temperature units."""
239 units = list(typing.get_args(Units))
242 if unit
not in self.
units:
243 raise ValueError(f
"invalid unit: {unit}")
244 self.
si: float = convert_to_si(
253 def value(self, unit: Units) -> float:
254 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
258 unit: Units |
None =
None,
259 locale: babel.Locale | GeoLocation |
None =
None,
260 template: str =
"{value} {unit}",
261 num_pattern: str =
"#,##0",
263 """Localized representation of a measured value.
265 If the ``unit`` is not set, an attempt is made to determine a ``unit``
266 matching the territory of the ``locale``. If the locale is not set, an
267 attempt is made to determine it from the HTTP request.
269 The value is converted into the respective unit before formatting.
271 The argument ``num_pattern`` is used to determine the string formatting
272 of the numerical value:
274 - https://babel.pocoo.org/en/latest/numbers.html#pattern-syntax
275 - https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns
277 The argument ``template`` specifies how the **string formatted** value
278 and unit are to be arranged.
280 - `Format Specification Mini-Language
281 <https://docs.python.org/3/library/string.html#format-specification-mini-language>`.
284 if isinstance(locale, GeoLocation):
285 locale = locale.locale()
291 if locale.territory
in [
"US"]:
293 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
294 return template.format(value=val_str, unit=unit)
299 """Class for converting pressure units and for string representation of
304 Units = typing.Literal[
"Pa",
"hPa",
"cm Hg",
"bar"]
305 """Supported units."""
307 units = list(typing.get_args(Units))
310 if unit
not in self.
units:
311 raise ValueError(f
"invalid unit: {unit}")
313 self.
si: float = convert_to_si(si_name=self.
si_name, symbol=unit, value=value)
318 def value(self, unit: Units) -> float:
319 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
323 unit: Units |
None =
None,
324 locale: babel.Locale | GeoLocation |
None =
None,
325 template: str =
"{value} {unit}",
326 num_pattern: str =
"#,##0",
328 if isinstance(locale, GeoLocation):
329 locale = locale.locale()
336 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
337 return template.format(value=val_str, unit=unit)
342 """Class for converting speed or velocity units and for string
343 representation of measured values.
347 Working with unit ``Bft`` (:py:obj:`searx.wikidata_units.Beaufort`) will
348 throw a :py:obj:`ValueError` for egative values or values greater 16 Bft
354 Units = typing.Literal[
"m/s",
"km/h",
"kn",
"mph",
"mi/h",
"Bft"]
355 """Supported units."""
357 units = list(typing.get_args(Units))
360 if unit
not in self.
units:
361 raise ValueError(f
"invalid unit: {unit}")
363 self.
si: float = convert_to_si(si_name=self.
si_name, symbol=unit, value=value)
368 def value(self, unit: Units) -> float:
369 return convert_from_si(si_name=self.
si_name, symbol=unit, value=self.
si)
373 unit: Units |
None =
None,
374 locale: babel.Locale | GeoLocation |
None =
None,
375 template: str =
"{value} {unit}",
376 num_pattern: str =
"#,##0",
378 if isinstance(locale, GeoLocation):
379 locale = locale.locale()
386 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
387 return template.format(value=val_str, unit=unit)
392 """Amount of relative humidity in the air. The unit is ``%``"""
394 Units = typing.Literal[
"%"]
395 """Supported unit."""
397 units = list(typing.get_args(Units))
410 locale: babel.Locale | GeoLocation |
None =
None,
411 template: str =
"{value}{unit}",
412 num_pattern: str =
"#,##0",
414 if isinstance(locale, GeoLocation):
415 locale = locale.locale()
420 val_str = babel.numbers.format_decimal(self.
value(), locale=locale, format=num_pattern)
421 return template.format(value=val_str, unit=unit)
426 """Class for converting compass points and azimuth values (360°)"""
428 Units = typing.Literal[
"°",
"Point"]
430 Point = typing.Literal[
431 "N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW"
433 """Compass point type definition"""
436 """Full turn (360°)"""
438 POINTS = list(typing.get_args(Point))
439 """Compass points."""
441 RANGE = TURN / len(POINTS)
442 """Angle sector of a compass point"""
445 if isinstance(azimuth, str):
446 if azimuth
not in self.
POINTS:
447 raise ValueError(f
"Invalid compass point: {azimuth}")
459 raise ValueError(f
"unknown unit: {unit}")
462 def point(cls, azimuth: float | int) -> Point:
463 """Returns the compass point to an azimuth value."""
464 azimuth = azimuth % cls.
TURN
467 azimuth = azimuth - cls.
RANGE / 2
468 idx = int(azimuth // cls.
RANGE)
473 unit: Units =
"Point",
474 locale: babel.Locale | GeoLocation |
None =
None,
475 template: str =
"{value}{unit}",
476 num_pattern: str =
"#,##0",
478 if isinstance(locale, GeoLocation):
479 locale = locale.locale()
484 val_str = self.
value(unit)
485 return template.format(value=val_str, unit=
"")
487 val_str = babel.numbers.format_decimal(self.
value(unit), locale=locale, format=num_pattern)
488 return template.format(value=val_str, unit=unit)
491WeatherConditionType = typing.Literal[
499 "light rain and thunder",
500 "light rain showers and thunder",
501 "light rain showers",
504 "rain showers and thunder",
507 "heavy rain and thunder",
508 "heavy rain showers and thunder",
509 "heavy rain showers",
512 "light sleet and thunder",
513 "light sleet showers and thunder",
514 "light sleet showers",
517 "sleet showers and thunder",
520 "heavy sleet and thunder",
521 "heavy sleet showers and thunder",
522 "heavy sleet showers",
525 "light snow and thunder",
526 "light snow showers and thunder",
527 "light snow showers",
530 "snow showers and thunder",
533 "heavy snow and thunder",
534 "heavy snow showers and thunder",
535 "heavy snow showers",
538"""Standardized designations for weather conditions. The designators were
539taken from a collaboration between NRK and Norwegian Meteorological Institute
540(yr.no_). `Weather symbols`_ can be assigned to the identifiers
541(weathericons_) and they are included in the translation (i18n/l10n
542:origin:`searx/searxng.msg`).
544.. _yr.no: https://www.yr.no/en
545.. _Weather symbols: https://github.com/nrkno/yr-weather-symbols
546.. _weathericons: https://github.com/metno/weathericons
549YR_WEATHER_SYMBOL_MAP = {
551 "partly cloudy":
"03d",
556 "light rain and thunder":
"30",
557 "light rain showers and thunder":
"24d",
558 "light rain showers":
"40d",
560 "rain and thunder":
"22",
561 "rain showers and thunder":
"06d",
562 "rain showers":
"05d",
564 "heavy rain and thunder":
"11",
565 "heavy rain showers and thunder":
"25d",
566 "heavy rain showers":
"41d",
569 "light sleet and thunder":
"31",
570 "light sleet showers and thunder":
"26d",
571 "light sleet showers":
"42d",
573 "sleet and thunder":
"23",
574 "sleet showers and thunder":
"20d",
575 "sleet showers":
"07d",
577 "heavy sleet and thunder":
"32",
578 "heavy sleet showers and thunder":
"27d",
579 "heavy sleet showers":
"43d",
582 "light snow and thunder":
"33",
583 "light snow showers and thunder":
"28d",
584 "light snow showers":
"44d",
586 "snow and thunder":
"14",
587 "snow showers and thunder":
"21d",
588 "snow showers":
"08d",
590 "heavy snow and thunder":
"34",
591 "heavy snow showers and thunder":
"29d",
592 "heavy snow showers":
"45d",
595"""Map a :py:obj:`WeatherConditionType` to a `YR weather symbol`_
599 base_url = "https://raw.githubusercontent.com/nrkno/yr-weather-symbols/refs/heads/master/symbols"
600 icon_url = f"{base_url}/outline/{YR_WEATHER_SYMBOL_MAP['sleet showers']}.svg"
602.. _YR weather symbol: https://github.com/nrkno/yr-weather-symbols/blob/master/locales/en.json
606if __name__ ==
"__main__":
609 for c
in typing.get_args(WeatherConditionType):
613 title =
"cached weather condition symbols"
615 print(
"=" * len(title))
616 print(_cache.state().report())
618 title = f
"properties of {_cache.cfg.name}"
620 print(
"=" * len(title))
621 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)
"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)