.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
weather.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""Implementations used for weather conditions and forecast."""
3# pylint: disable=too-few-public-methods
4
5__all__ = [
6 "symbol_url",
7 "Temperature",
8 "Pressure",
9 "WindSpeed",
10 "RelativeHumidity",
11 "Compass",
12 "WeatherConditionType",
13 "DateTime",
14 "GeoLocation",
15]
16
17import typing
18
19import base64
20import datetime
21import dataclasses
22import zoneinfo
23
24from urllib.parse import quote_plus
25
26import babel
27import babel.numbers
28import babel.dates
29import babel.languages
30import flask_babel
31
32from searx import network
33from searx.cache import ExpireCache, ExpireCacheCfg
34from searx.extended_types import sxng_request
35from searx.wikidata_units import convert_to_si, convert_from_si
36
37WEATHER_DATA_CACHE: ExpireCache | None = None
38"""A simple cache for weather data (geo-locations, icons, ..)"""
39
40YR_WEATHER_SYMBOL_URL = "https://raw.githubusercontent.com/nrkno/yr-weather-symbols/refs/heads/master/symbols/outline"
41
42
44
45 global WEATHER_DATA_CACHE # pylint: disable=global-statement
46
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, # max. 200kB per icon (icons have most often 10-20kB)
52 MAXHOLD_TIME=60 * 60 * 24 * 7 * 4, # 4 weeks
53 )
54 )
55 return WEATHER_DATA_CACHE
56
57
59 # The function should return a locale (the sxng-tag: de-DE.en-US, ..) that
60 # can later be used to format and convert measured values for the output of
61 # weather data to the user.
62 #
63 # In principle, SearXNG only has two possible parameters for determining
64 # the locale: the UI language or the search- language/region. Since the
65 # conversion of weather data and time information is usually
66 # region-specific, the UI language is not suitable.
67 #
68 # It would probably be ideal to use the user's geolocation, but this will
69 # probably never be available in SearXNG (privacy critical).
70 #
71 # Therefore, as long as no "better" parameters are available, this function
72 # returns a locale based on the search region.
73
74 # pylint: disable=import-outside-toplevel,disable=cyclic-import
75 from searx import query
76 from searx.preferences import ClientPref
77
78 query = query.RawTextQuery(sxng_request.form.get("q", ""), [])
79 if query.languages and query.languages[0] not in ["all", "auto"]:
80 return query.languages[0]
81
82 search_lang = sxng_request.form.get("language")
83 if search_lang and search_lang not in ["all", "auto"]:
84 return search_lang
85
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"]:
89 return search_lang
90 return "en"
91
92
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`.
96
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
99 """
100 # Symbols for darkmode/lightmode? .. and day/night symbols? .. for the
101 # latter we need a geopoint (critical in sense of privacy)
102
103 fname = YR_WEATHER_SYMBOL_MAP.get(condition)
104 if fname is None:
105 return None
106
107 ctx = "weather_symbol_url"
108 cache = get_WEATHER_DATA_CACHE()
109 origin_url = f"{YR_WEATHER_SYMBOL_URL}/{fname}.svg"
110
111 data_url = cache.get(origin_url, ctx=ctx)
112 if data_url is not None:
113 return data_url
114
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)
120 return data_url
121
122
123@dataclasses.dataclass
125 """Minimal implementation of Geocoding."""
126
127 # The type definition was based on the properties from the geocoding API of
128 # open-meteo.
129 #
130 # - https://open-meteo.com/en/docs/geocoding-api
131 # - https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
132 # - https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
133
134 name: str
135 latitude: float # Geographical WGS84 coordinates of this location
136 longitude: float
137 elevation: float # Elevation above mean sea level of this location
138 country_code: str # 2-Character ISO-3166-1 alpha2 country code. E.g. DE for Germany
139 timezone: str # Time zone using time zone database definitions
140
141 @property
142 def zoneinfo(self) -> zoneinfo.ZoneInfo:
143 return zoneinfo.ZoneInfo(self.timezone)
144
145 def __str__(self):
146 return self.name
147
148 def locale(self) -> babel.Locale:
149
150 # by region of the search language
151 sxng_tag = _get_sxng_locale_tag()
152 if "-" in sxng_tag:
153 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
154 return locale
155
156 # by most popular language in the region (country code)
157 for lang in babel.languages.get_official_languages(self.country_code):
158 try:
159 locale = babel.Locale.parse(f"{lang}_{self.country_code}")
160 return locale
161 except babel.UnknownLocaleError:
162 continue
163
164 # No locale could be determined. This does not actually occur, but if
165 # it does, the English language is used by default. But not region US.
166 # US has some units that are only used in US but not in the rest of the
167 # world (e.g. °F instead of °C)
168 return babel.Locale("en", territory="DE")
169
170 @classmethod
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`
174 is thrown.
175 """
176
177 ctx = "weather_geolocation_by_query"
178 cache = get_WEATHER_DATA_CACHE()
179 geo_props = cache.get(search_term, ctx=ctx)
180
181 if not geo_props:
182 geo_props = cls._query_open_meteo(search_term=search_term)
183 cache.set(key=search_term, value=geo_props, expire=None, ctx=ctx)
184
185 return cls(**geo_props) # type: ignore
186
187 @classmethod
188 def _query_open_meteo(cls, search_term: str) -> dict[str, str]:
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")
194 if not 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)}
198
199
200DateTimeFormats = typing.Literal["full", "long", "medium", "short"]
201DateTimeLocaleTypes = typing.Literal["UI"]
202
203
204@typing.final
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).
210
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``.
217 """
218
219 def __init__(self, time: datetime.datetime):
220 self.datetime = time
221
222 def __str__(self):
223 return self.l10n()
224
225 def l10n(
226 self,
227 fmt: DateTimeFormats | str = "medium",
228 locale: DateTimeLocaleTypes | babel.Locale | GeoLocation | None = None,
229 ) -> str:
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()
235 elif locale is None:
236 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
237 return babel.dates.format_datetime(self.datetime, format=fmt, locale=locale)
238
240 self,
241 fmt: DateTimeFormats | str = "medium",
242 locale: DateTimeLocaleTypes | babel.Locale | GeoLocation | None = None,
243 ) -> str:
244 """Localized representation of date."""
245
246 if isinstance(locale, str) and locale == "UI":
247 locale = flask_babel.get_locale()
248 elif isinstance(locale, GeoLocation):
249 locale = locale.locale()
250 elif locale is None:
251 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
252 return babel.dates.format_date(self.datetime, format=fmt, locale=locale)
253
254
255@typing.final
257 """Class for converting temperature units and for string representation of
258 measured values."""
259
260 si_name = "Q11579"
261
262 Units = typing.Literal["°C", "°F", "K"]
263 """Supported temperature units."""
264
265 units = list(typing.get_args(Units))
266
267 def __init__(self, value: float, unit: Units):
268 if unit not in self.units:
269 raise ValueError(f"invalid unit: {unit}")
270 self.si: float = convert_to_si( # pylint: disable=invalid-name
271 si_name=self.si_name,
272 symbol=unit,
273 value=value,
274 )
275
276 def __str__(self):
277 return self.l10n()
278
279 def value(self, unit: Units) -> float:
280 return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si)
281
282 def l10n(
283 self,
284 unit: Units | None = None,
285 locale: babel.Locale | GeoLocation | None = None,
286 template: str = "{value} {unit}",
287 num_pattern: str = "#,##0",
288 ) -> str:
289 """Localized representation of a measured value.
290
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.
294
295 The value is converted into the respective unit before formatting.
296
297 The argument ``num_pattern`` is used to determine the string formatting
298 of the numerical value:
299
300 - https://babel.pocoo.org/en/latest/numbers.html#pattern-syntax
301 - https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns
302
303 The argument ``template`` specifies how the **string formatted** value
304 and unit are to be arranged.
305
306 - `Format Specification Mini-Language
307 <https://docs.python.org/3/library/string.html#format-specification-mini-language>`.
308 """
309
310 if isinstance(locale, GeoLocation):
311 locale = locale.locale()
312 elif locale is None:
313 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
314
315 if unit is None: # unit by territory
316 unit = "°C"
317 if locale.territory in ["US"]:
318 unit = "°F"
319 val_str = babel.numbers.format_decimal(self.value(unit), locale=locale, format=num_pattern)
320 return template.format(value=val_str, unit=unit)
321
322
323@typing.final
325 """Class for converting pressure units and for string representation of
326 measured values."""
327
328 si_name = "Q44395"
329
330 Units = typing.Literal["Pa", "hPa", "cm Hg", "bar"]
331 """Supported units."""
332
333 units = list(typing.get_args(Units))
334
335 def __init__(self, value: float, unit: Units):
336 if unit not in self.units:
337 raise ValueError(f"invalid unit: {unit}")
338 # pylint: disable=invalid-name
339 self.si: float = convert_to_si(si_name=self.si_name, symbol=unit, value=value)
340
341 def __str__(self):
342 return self.l10n()
343
344 def value(self, unit: Units) -> float:
345 return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si)
346
347 def l10n(
348 self,
349 unit: Units | None = None,
350 locale: babel.Locale | GeoLocation | None = None,
351 template: str = "{value} {unit}",
352 num_pattern: str = "#,##0",
353 ) -> str:
354 if isinstance(locale, GeoLocation):
355 locale = locale.locale()
356 elif locale is None:
357 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
358
359 if unit is None: # unit by territory?
360 unit = "hPa"
361
362 val_str = babel.numbers.format_decimal(self.value(unit), locale=locale, format=num_pattern)
363 return template.format(value=val_str, unit=unit)
364
365
366@typing.final
368 """Class for converting speed or velocity units and for string
369 representation of measured values.
370
371 .. hint::
372
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
375 (55.6 m/s)
376 """
377
378 si_name = "Q182429"
379
380 Units = typing.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"]
381 """Supported units."""
382
383 units = list(typing.get_args(Units))
384
385 def __init__(self, value: float, unit: Units):
386 if unit not in self.units:
387 raise ValueError(f"invalid unit: {unit}")
388 # pylint: disable=invalid-name
389 self.si: float = convert_to_si(si_name=self.si_name, symbol=unit, value=value)
390
391 def __str__(self):
392 return self.l10n()
393
394 def value(self, unit: Units) -> float:
395 return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si)
396
397 def l10n(
398 self,
399 unit: Units | None = None,
400 locale: babel.Locale | GeoLocation | None = None,
401 template: str = "{value} {unit}",
402 num_pattern: str = "#,##0",
403 ) -> str:
404 if isinstance(locale, GeoLocation):
405 locale = locale.locale()
406 elif locale is None:
407 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
408
409 if unit is None: # unit by territory?
410 unit = "m/s"
411
412 val_str = babel.numbers.format_decimal(self.value(unit), locale=locale, format=num_pattern)
413 return template.format(value=val_str, unit=unit)
414
415
416@typing.final
418 """Amount of relative humidity in the air. The unit is ``%``"""
419
420 Units = typing.Literal["%"]
421 """Supported unit."""
422
423 units = list(typing.get_args(Units))
424
425 def __init__(self, humidity: float):
426 self.humidity = humidity
427
428 def __str__(self):
429 return self.l10n()
430
431 def value(self) -> float:
432 return self.humidity
433
434 def l10n(
435 self,
436 locale: babel.Locale | GeoLocation | None = None,
437 template: str = "{value}{unit}",
438 num_pattern: str = "#,##0",
439 ) -> str:
440 if isinstance(locale, GeoLocation):
441 locale = locale.locale()
442 elif locale is None:
443 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
444
445 unit = "%"
446 val_str = babel.numbers.format_decimal(self.value(), locale=locale, format=num_pattern)
447 return template.format(value=val_str, unit=unit)
448
449
450@typing.final
452 """Class for converting compass points and azimuth values (360°)"""
453
454 Units = typing.Literal["°", "Point"]
455
456 Point = typing.Literal[
457 "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
458 ]
459 """Compass point type definition"""
460
461 TURN = 360.0
462 """Full turn (360°)"""
463
464 POINTS = list(typing.get_args(Point))
465 """Compass points."""
466
467 RANGE = TURN / len(POINTS)
468 """Angle sector of a compass point"""
469
470 def __init__(self, azimuth: float | int | Point):
471 if isinstance(azimuth, str):
472 if azimuth not in self.POINTS:
473 raise ValueError(f"Invalid compass point: {azimuth}")
474 azimuth = self.POINTS.index(azimuth) * self.RANGE
475 self.azimuth = azimuth % self.TURN
476
477 def __str__(self):
478 return self.l10n()
479
480 def value(self, unit: Units):
481 if unit == "Point":
482 return self.point(self.azimuth)
483 if unit == "°":
484 return self.azimuth
485 raise ValueError(f"unknown unit: {unit}")
486
487 @classmethod
488 def point(cls, azimuth: float | int) -> Point:
489 """Returns the compass point to an azimuth value."""
490 azimuth = azimuth % cls.TURN
491 # The angle sector of a compass point starts 1/2 sector range before
492 # and after compass point (example: "N" goes from -11.25° to +11.25°)
493 azimuth = azimuth - cls.RANGE / 2
494 idx = int(azimuth // cls.RANGE)
495 return cls.POINTS[idx]
496
497 def l10n(
498 self,
499 unit: Units = "Point",
500 locale: babel.Locale | GeoLocation | None = None,
501 template: str = "{value}{unit}",
502 num_pattern: str = "#,##0",
503 ) -> str:
504 if isinstance(locale, GeoLocation):
505 locale = locale.locale()
506 elif locale is None:
507 locale = babel.Locale.parse(_get_sxng_locale_tag(), sep='-')
508
509 if unit == "Point":
510 val_str = self.value(unit)
511 return template.format(value=val_str, unit="")
512
513 val_str = babel.numbers.format_decimal(self.value(unit), locale=locale, format=num_pattern)
514 return template.format(value=val_str, unit=unit)
515
516
517WeatherConditionType = typing.Literal[
518 # The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel")
519 "clear sky",
520 "partly cloudy",
521 "cloudy",
522 "fair",
523 "fog",
524 # rain
525 "light rain and thunder",
526 "light rain showers and thunder",
527 "light rain showers",
528 "light rain",
529 "rain and thunder",
530 "rain showers and thunder",
531 "rain showers",
532 "rain",
533 "heavy rain and thunder",
534 "heavy rain showers and thunder",
535 "heavy rain showers",
536 "heavy rain",
537 # sleet
538 "light sleet and thunder",
539 "light sleet showers and thunder",
540 "light sleet showers",
541 "light sleet",
542 "sleet and thunder",
543 "sleet showers and thunder",
544 "sleet showers",
545 "sleet",
546 "heavy sleet and thunder",
547 "heavy sleet showers and thunder",
548 "heavy sleet showers",
549 "heavy sleet",
550 # snow
551 "light snow and thunder",
552 "light snow showers and thunder",
553 "light snow showers",
554 "light snow",
555 "snow and thunder",
556 "snow showers and thunder",
557 "snow showers",
558 "snow",
559 "heavy snow and thunder",
560 "heavy snow showers and thunder",
561 "heavy snow showers",
562 "heavy snow",
563]
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`).
569
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
573"""
574
575YR_WEATHER_SYMBOL_MAP = {
576 "clear sky": "01d", # 01d clearsky_day
577 "partly cloudy": "03d", # 03d partlycloudy_day
578 "cloudy": "04", # 04 cloudy
579 "fair": "02d", # 02d fair_day
580 "fog": "15", # 15 fog
581 # rain
582 "light rain and thunder": "30", # 30 lightrainandthunder
583 "light rain showers and thunder": "24d", # 24d lightrainshowersandthunder_day
584 "light rain showers": "40d", # 40d lightrainshowers_day
585 "light rain": "46", # 46 lightrain
586 "rain and thunder": "22", # 22 rainandthunder
587 "rain showers and thunder": "06d", # 06d rainshowersandthunder_day
588 "rain showers": "05d", # 05d rainshowers_day
589 "rain": "09", # 09 rain
590 "heavy rain and thunder": "11", # 11 heavyrainandthunder
591 "heavy rain showers and thunder": "25d", # 25d heavyrainshowersandthunder_day
592 "heavy rain showers": "41d", # 41d heavyrainshowers_day
593 "heavy rain": "10", # 10 heavyrain
594 # sleet
595 "light sleet and thunder": "31", # 31 lightsleetandthunder
596 "light sleet showers and thunder": "26d", # 26d lightssleetshowersandthunder_day
597 "light sleet showers": "42d", # 42d lightsleetshowers_day
598 "light sleet": "47", # 47 lightsleet
599 "sleet and thunder": "23", # 23 sleetandthunder
600 "sleet showers and thunder": "20d", # 20d sleetshowersandthunder_day
601 "sleet showers": "07d", # 07d sleetshowers_day
602 "sleet": "12", # 12 sleet
603 "heavy sleet and thunder": "32", # 32 heavysleetandthunder
604 "heavy sleet showers and thunder": "27d", # 27d heavysleetshowersandthunder_day
605 "heavy sleet showers": "43d", # 43d heavysleetshowers_day
606 "heavy sleet": "48", # 48 heavysleet
607 # snow
608 "light snow and thunder": "33", # 33 lightsnowandthunder
609 "light snow showers and thunder": "28d", # 28d lightssnowshowersandthunder_day
610 "light snow showers": "44d", # 44d lightsnowshowers_day
611 "light snow": "49", # 49 lightsnow
612 "snow and thunder": "14", # 14 snowandthunder
613 "snow showers and thunder": "21d", # 21d snowshowersandthunder_day
614 "snow showers": "08d", # 08d snowshowers_day
615 "snow": "13", # 13 snow
616 "heavy snow and thunder": "34", # 34 heavysnowandthunder
617 "heavy snow showers and thunder": "29d", # 29d heavysnowshowersandthunder_day
618 "heavy snow showers": "45d", # 45d heavysnowshowers_day
619 "heavy snow": "50", # 50 heavysnow
620}
621"""Map a :py:obj:`WeatherConditionType` to a `YR weather symbol`_
622
623.. code::
624
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"
627
628.. _YR weather symbol: https://github.com/nrkno/yr-weather-symbols/blob/master/locales/en.json
629
630"""
631
632if __name__ == "__main__":
633
634 # test: fetch all symbols of the type catalog ..
635 for c in typing.get_args(WeatherConditionType):
636 symbol_url(condition=c)
637
639 title = "cached weather condition symbols"
640 print(title)
641 print("=" * len(title))
642 print(_cache.state().report())
643 print()
644 title = f"properties of {_cache.cfg.name}"
645 print(title)
646 print("=" * len(title))
647 print(str(_cache.properties)) # type: ignore
__init__(self, float|int|Point azimuth)
Definition weather.py:470
str l10n(self, Units unit="Point", babel.Locale|GeoLocation|None locale=None, str template="{value}{unit}", str num_pattern="#,##0")
Definition weather.py:503
Point point(cls, float|int azimuth)
Definition weather.py:488
value(self, Units unit)
Definition weather.py:480
str l10n_date(self, DateTimeFormats|str fmt="medium", DateTimeLocaleTypes|babel.Locale|GeoLocation|None locale=None)
Definition weather.py:243
str l10n(self, DateTimeFormats|str fmt="medium", DateTimeLocaleTypes|babel.Locale|GeoLocation|None locale=None)
Definition weather.py:229
__init__(self, datetime.datetime time)
Definition weather.py:219
"GeoLocation" by_query(cls, str search_term)
Definition weather.py:171
dict[str, str] _query_open_meteo(cls, str search_term)
Definition weather.py:188
babel.Locale locale(self)
Definition weather.py:148
zoneinfo.ZoneInfo zoneinfo(self)
Definition weather.py:142
str l10n(self, Units|None unit=None, babel.Locale|GeoLocation|None locale=None, str template="{value} {unit}", str num_pattern="#,##0")
Definition weather.py:353
float value(self, Units unit)
Definition weather.py:344
__init__(self, float value, Units unit)
Definition weather.py:335
__init__(self, float humidity)
Definition weather.py:425
str l10n(self, babel.Locale|GeoLocation|None locale=None, str template="{value}{unit}", str num_pattern="#,##0")
Definition weather.py:439
str l10n(self, Units|None unit=None, babel.Locale|GeoLocation|None locale=None, str template="{value} {unit}", str num_pattern="#,##0")
Definition weather.py:288
__init__(self, float value, Units unit)
Definition weather.py:267
float value(self, Units unit)
Definition weather.py:279
str l10n(self, Units|None unit=None, babel.Locale|GeoLocation|None locale=None, str template="{value} {unit}", str num_pattern="#,##0")
Definition weather.py:403
__init__(self, float value, Units unit)
Definition weather.py:385
float value(self, Units unit)
Definition weather.py:394
get_WEATHER_DATA_CACHE()
Definition weather.py:43
str _get_sxng_locale_tag()
Definition weather.py:58
str|None symbol_url("WeatherConditionType" condition)
Definition weather.py:93