python-weather 2.0.4__py3-none-any.whl → 2.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,7 +31,7 @@ __title__ = 'python-weather'
31
31
  __author__ = 'null8626'
32
32
  __license__ = 'MIT'
33
33
  __copyright__ = 'Copyright (c) 2021-2024 null8626'
34
- __version__ = '2.0.4'
34
+ __version__ = '2.0.6'
35
35
  __all__ = (
36
36
  'METRIC',
37
37
  'IMPERIAL',
python_weather/base.py CHANGED
@@ -22,7 +22,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  SOFTWARE.
23
23
  """
24
24
 
25
- from enum import auto
26
25
  from typing import Tuple
27
26
 
28
27
  from .errors import Error
@@ -38,7 +37,7 @@ class CustomizableBase:
38
37
  self.locale = locale
39
38
 
40
39
  @property
41
- def unit(self) -> auto:
40
+ def unit(self) -> _Unit:
42
41
  """The measuring unit used to display information in this object."""
43
42
 
44
43
  return self.__unit
@@ -68,9 +67,9 @@ class CustomizableBase:
68
67
  """
69
68
  Sets the default localization used to display information in this object.
70
69
 
71
- :param to: The new :class:`Locale` to be used to display information in this object.
70
+ :param to: The new :class:`~python_weather.enums.Locale` to be used to display information in this object.
72
71
  :type to: Locale
73
- :exception Error: If the ``to`` argument is not a part of the :class:`Locale` enum.
72
+ :exception Error: If the ``to`` argument is not a part of the :class:`~python_weather.enums.Locale` enum.
74
73
  """
75
74
 
76
75
  if not isinstance(to, Locale):
@@ -79,84 +78,71 @@ class CustomizableBase:
79
78
  self.__locale = to
80
79
 
81
80
 
82
- class BaseForecast(CustomizableBase):
83
- __slots__: Tuple[str, ...] = ('__inner',)
81
+ class BaseForecast:
82
+ __slots__: Tuple[str, ...] = (
83
+ 'ultraviolet',
84
+ 'humidity',
85
+ 'wind_direction',
86
+ 'kind',
87
+ 'feels_like',
88
+ 'temperature',
89
+ 'precipitation',
90
+ 'pressure',
91
+ 'visibility',
92
+ 'wind_speed',
93
+ 'description',
94
+ )
84
95
 
85
- def __init__(self, json: dict, unit: _Unit, locale: Locale):
86
- self.__inner = json
87
-
88
- super().__init__(unit, locale)
89
-
90
- @property
91
- def ultraviolet(self) -> UltraViolet:
92
- """The ultra-violet (UV) index."""
93
-
94
- return UltraViolet._new(int(self.__inner['uvIndex']))
95
-
96
- @property
97
- def feels_like(self) -> int:
98
- """What it felt like, in celcius or fahrenheit."""
96
+ ultraviolet: UltraViolet
97
+ """The ultra-violet (UV) index."""
99
98
 
100
- return int(self.__inner[f'FeelsLike{self._CustomizableBase__unit.temperature}'])
99
+ humidity: int
100
+ """The humidity value in percent."""
101
101
 
102
- @property
103
- def humidity(self) -> int:
104
- """The humidity value in percent."""
102
+ wind_direction: WindDirection
103
+ """The wind direction."""
105
104
 
106
- return int(self.__inner['humidity'])
105
+ kind: Kind
106
+ """The kind of the forecast."""
107
107
 
108
- @property
109
- def temperature(self) -> int:
110
- """The temperature in either celcius or Fahrenheit."""
108
+ feels_like: int
109
+ """What it felt like, in celcius or fahrenheit."""
111
110
 
112
- return int(self.__inner[f'temp_{self._CustomizableBase__unit.temperature}'])
111
+ temperature: int
112
+ """The temperature in either celcius or Fahrenheit."""
113
113
 
114
- @property
115
- def precipitation(self) -> float:
116
- """The precipitation in either millimeters or inches."""
114
+ precipitation: float
115
+ """The precipitation in either millimeters or inches."""
117
116
 
118
- return float(self.__inner[f'precip{self._CustomizableBase__unit.precipitation}'])
117
+ pressure: float
118
+ """The pressure in either pascal or inches."""
119
119
 
120
- @property
121
- def pressure(self) -> float:
122
- """The pressure in either pascal or inches."""
120
+ visibility: int
121
+ """The visibility distance in either kilometers or miles."""
123
122
 
124
- return float(self.__inner[f'pressure{self._CustomizableBase__unit.pressure}'])
123
+ wind_speed: int
124
+ """The wind speeds in either kilometers/hour or miles/hour."""
125
125
 
126
- @property
127
- def visibility(self) -> int:
128
- """The visibility distance in either kilometers or miles."""
129
-
130
- return int(self.__inner[f'visibility{self._CustomizableBase__unit.visibility}'])
131
-
132
- @property
133
- def wind_speed(self) -> int:
134
- """The wind speeds in either kilometers/hour or miles/hour."""
135
-
136
- return int(self.__inner[f'windspeed{self._CustomizableBase__unit.velocity}'])
137
-
138
- @property
139
- def wind_direction(self) -> WindDirection:
140
- """The wind direction."""
141
-
142
- return WindDirection._new(
143
- self.__inner['winddir16Point'], int(self.__inner['winddirDegree'])
144
- )
145
-
146
- @property
147
- def description(self) -> str:
148
- """The description regarding the forecast. This can be localized in different languages depending on the localization used."""
126
+ description: str
127
+ """The description regarding the forecast. This can be localized in different languages depending on the localization used."""
149
128
 
129
+ def __init__(self, json: dict, unit: _Unit, locale: Locale):
150
130
  description = (
151
- self.__inner['weatherDesc'][0]['value']
152
- if self._CustomizableBase__locale == Locale.ENGLISH
153
- else self.__inner[f'lang_{self._CustomizableBase__locale.value}'][0]['value']
131
+ json['weatherDesc'][0]['value']
132
+ if locale is Locale.ENGLISH
133
+ else json[f'lang_{locale.value}'][0]['value']
154
134
  )
155
135
 
156
- return description.strip()
157
-
158
- @property
159
- def kind(self) -> Kind:
160
- """The kind of the forecast."""
161
-
162
- return Kind(int(self.__inner['weatherCode']))
136
+ self.ultraviolet = UltraViolet._new(int(json['uvIndex']))
137
+ self.humidity = int(json['humidity'])
138
+ self.wind_direction = WindDirection._new(
139
+ json['winddir16Point'], int(json['winddirDegree'])
140
+ )
141
+ self.kind = Kind(int(json['weatherCode']))
142
+ self.feels_like = int(json[f'FeelsLike{unit.temperature}'])
143
+ self.temperature = int(json[f'temp_{unit.temperature}'])
144
+ self.precipitation = float(json[f'precip{unit.precipitation}'])
145
+ self.pressure = float(json[f'pressure{unit.pressure}'])
146
+ self.visibility = int(json[f'visibility{unit.visibility}'])
147
+ self.wind_speed = int(json[f'windspeed{unit.velocity}'])
148
+ self.description = description.strip()
python_weather/client.py CHANGED
@@ -23,10 +23,9 @@ SOFTWARE.
23
23
  """
24
24
 
25
25
  from aiohttp import ClientSession, ClientTimeout, TCPConnector
26
+ from typing import Optional, Tuple, Self
26
27
  from urllib.parse import quote_plus
27
- from typing import Optional, Tuple
28
28
  from asyncio import sleep
29
- from enum import auto
30
29
 
31
30
  from .errors import Error, RequestError
32
31
  from .constants import _Unit, METRIC
@@ -40,32 +39,35 @@ class Client(CustomizableBase):
40
39
  The class that lets you interact with the API.
41
40
 
42
41
  :param unit: Whether to use the metric or imperial/customary system (``IMPERIAL``). Defaults to ``METRIC``.
43
- :type unit: Optional[:py:class:`enum.auto`]
42
+ :type unit: _Unit
44
43
  :param locale: Whether to use a different locale/language as the description for the returned forecast. Defaults to ``Locale.ENGLISH``.
45
- :type locale: Optional[Locale]
46
- :param session: Whether to use an existing aiohttp client session for requesting or not. Defaults to ``None`` (creates a new one instead)
47
- :type session: Optional[:class:`aiohttp.ClientSession`]
44
+ :type locale: Locale
45
+ :param session: Whether to use an existing aiohttp client session for requesting or not. Defaults to ``None`` (creates a new one instead).
46
+ :type session: Optional[:class:`~aiohttp.ClientSession`]
47
+ :param max_retries: Maximum amount of retries upon receiving HTTP request failure before raising a :class:`~python_weather.errors.RequestError`. To have infinite retries, use ``-1`` (NOT recommended). Defaults to ``None`` (or 3 retries).
48
+ :type max_retries: Optional[:class:`int`]
48
49
 
49
- :raises Error: If ``unit`` is not ``METRIC`` or ``IMPERIAL``, or if ``locale`` is not ``None`` and not a part of the :class:`Locale` enum.
50
+ :raises Error: If ``unit`` is not ``METRIC`` or ``IMPERIAL``, or if ``locale`` is not ``None`` and not a part of the :class:`~python_weather.enums.Locale` enum.
50
51
  """
51
52
 
52
- __slots__: Tuple[str, ...] = ('__own_session', '__session')
53
+ __slots__: Tuple[str, ...] = ('__own_session', '__session', '__max_retries')
53
54
 
54
55
  def __init__(
55
56
  self,
56
57
  *,
57
- unit: Optional[auto] = METRIC,
58
- locale: Optional[Locale] = Locale.ENGLISH,
58
+ unit: _Unit = METRIC,
59
+ locale: Locale = Locale.ENGLISH,
59
60
  session: Optional[ClientSession] = None,
61
+ max_retries: Optional[int] = None,
60
62
  ):
61
63
  super().__init__(unit, locale)
62
64
 
63
65
  self.__own_session = session is None
64
66
  self.__session = session or ClientSession(
65
- raise_for_status=True,
66
67
  timeout=ClientTimeout(total=5000.0),
67
68
  connector=TCPConnector(verify_ssl=False),
68
69
  )
70
+ self.__max_retries = max_retries or 3
69
71
 
70
72
  def __repr__(self) -> str:
71
73
  return f'<{__class__.__name__} {self.__session!r}>'
@@ -74,7 +76,7 @@ class Client(CustomizableBase):
74
76
  self,
75
77
  location: str,
76
78
  *,
77
- unit: Optional[auto] = None,
79
+ unit: Optional[_Unit] = None,
78
80
  locale: Optional[Locale] = None,
79
81
  ) -> Forecast:
80
82
  """
@@ -83,11 +85,12 @@ class Client(CustomizableBase):
83
85
  :param location: The requested location name for said weather forecast.
84
86
  :type location: str
85
87
  :param unit: Overrides the unit used by this object. Defaults to the one used by this object.
86
- :type unit: Optional[:py:class:`enum.auto`]
88
+ :type unit: Optional[``_Unit``]
87
89
  :param locale: Overrides the locale used by this object. Defaults to the one used by this object.
88
- :type locale: Optional[Locale]
90
+ :type locale: Optional[:class:`~python_weather.enums.Locale`]
89
91
 
90
- :exception Error: If the aiohttp client session used by the :class:`Client` object is already closed, if the ``unit`` argument is not ``None`` and it's also not ``METRIC`` or ``IMPERIAL``, if the ``locale`` argument is not ``None`` and it's also not a part of the :class:`Locale` enum, or if the :class:`Client` cannot send a web request to the web server.
92
+ :exception Error: If the aiohttp client session used by the :class:`~python_weather.client.Client` object is already closed, if the ``unit`` argument is not ``None`` and it's also not ``METRIC`` or ``IMPERIAL``, if the ``locale`` argument is not ``None`` and it's also not a part of the :class:`~python_weather.enums.Locale` enum.
93
+ :exception RequestError: If the :class:`~python_weather.client.Client` can't send a web request to the web server.
91
94
 
92
95
  :returns: The requested weather forecast.
93
96
  :rtype: Forecast
@@ -106,28 +109,32 @@ class Client(CustomizableBase):
106
109
 
107
110
  subdomain = f'{locale.value}.' if locale != Locale.ENGLISH else ''
108
111
  delay = 0.5
112
+ attempts = 0
109
113
 
110
114
  while True:
111
115
  try:
112
116
  async with self.__session.get(
113
117
  f'https://{subdomain}wttr.in/{quote_plus(location)}?format=j1'
114
118
  ) as resp:
119
+ resp.raise_for_status()
120
+
115
121
  return Forecast(await resp.json(), unit, locale)
116
122
  except Exception as err:
117
- if delay == 4:
123
+ if attempts == self.__max_retries:
118
124
  raise RequestError(err)
119
125
 
120
126
  await sleep(delay)
127
+ attempts += 1
121
128
  delay *= 2
122
129
 
123
130
  async def close(self) -> None:
124
- """Closes the :class:`Client` object. Nothing will happen if the client uses a pre-existing :class:`aiohttp.ClientSession` or if the session is already closed."""
131
+ """Closes the :class:`~python_weather.client.Client` object. Nothing will happen if the client uses a pre-existing :class:`~aiohttp.ClientSession` or if the session is already closed."""
125
132
 
126
133
  if self.__own_session and not self.__session.closed:
127
134
  await self.__session.close()
128
135
 
129
- async def __aenter__(self) -> 'Client':
136
+ async def __aenter__(self) -> Self:
130
137
  return self
131
138
 
132
- async def __aexit__(self, *_, **__):
139
+ async def __aexit__(self, *_, **__) -> None:
133
140
  await self.close()
python_weather/enums.py CHANGED
@@ -23,7 +23,7 @@ SOFTWARE.
23
23
  """
24
24
 
25
25
  from enum import Enum
26
- from typing import Union, Tuple
26
+ from typing import Union, Tuple, Self
27
27
 
28
28
  from .constants import WIND_DIRECTION_EMOJIS
29
29
 
@@ -70,14 +70,14 @@ class HeatIndex(IndexedEnum):
70
70
  DANGER = None
71
71
  EXTREME_DANGER = None
72
72
 
73
- def _new(celcius_index: int, true_index: int) -> 'HeatIndex':
73
+ def _new(celcius_index: int, true_index: int) -> Self:
74
74
  enum = HeatIndex(celcius_index)
75
75
  enum.index = true_index
76
76
 
77
77
  return enum
78
78
 
79
79
  @classmethod
80
- def _missing_(self, celcius_index: int) -> 'HeatIndex':
80
+ def _missing_(self, celcius_index: int) -> Self:
81
81
  if celcius_index <= 32:
82
82
  return self.CAUTION
83
83
  elif celcius_index <= 39:
@@ -99,14 +99,14 @@ class UltraViolet(BasicEnum, IndexedEnum):
99
99
  VERY_HIGH = None
100
100
  EXTREME = None
101
101
 
102
- def _new(index: int) -> 'UltraViolet':
102
+ def _new(index: int) -> Self:
103
103
  enum = UltraViolet(index)
104
104
  enum.index = index
105
105
 
106
106
  return enum
107
107
 
108
108
  @classmethod
109
- def _missing_(self, index: int) -> 'UltraViolet':
109
+ def _missing_(self, index: int) -> Self:
110
110
  if index <= 2:
111
111
  return self.LOW
112
112
  elif index <= 5:
@@ -122,7 +122,7 @@ class UltraViolet(BasicEnum, IndexedEnum):
122
122
  class WindDirection(BasicEnum):
123
123
  """Represents a wind direction."""
124
124
 
125
- __slots__: Tuple[str, ...] = ('__degrees',)
125
+ __slots__: Tuple[str, ...] = ('degrees',)
126
126
 
127
127
  NORTH = 'N'
128
128
  NORTH_NORTHEAST = 'NNE'
@@ -141,9 +141,12 @@ class WindDirection(BasicEnum):
141
141
  NORTHWEST = 'NW'
142
142
  NORTH_NORTHWEST = 'NNW'
143
143
 
144
- def _new(value: str, degrees: float) -> 'WindDirection':
144
+ degrees: float
145
+ """The wind direction's value in degrees."""
146
+
147
+ def _new(value: str, degrees: float) -> Self:
145
148
  enum = WindDirection(value)
146
- enum.__degrees = degrees
149
+ enum.degrees = degrees
147
150
 
148
151
  return enum
149
152
 
@@ -183,20 +186,17 @@ class WindDirection(BasicEnum):
183
186
  else:
184
187
  return 326.25 < other <= 348.75
185
188
 
186
- def __float__(self) -> float:
187
- return self.__degrees
188
-
189
- @property
190
- def degrees(self) -> int:
191
- """The wind direction's value in degrees."""
189
+ def __int__(self) -> int:
190
+ return int(self.degrees)
192
191
 
193
- return self.__degrees
192
+ def __float__(self) -> float:
193
+ return self.degrees
194
194
 
195
195
  @property
196
196
  def emoji(self) -> str:
197
197
  """The emoji representing this enum."""
198
198
 
199
- return WIND_DIRECTION_EMOJIS[int(((self.__degrees + 22.5) % 360) / 45.0)]
199
+ return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360) / 45.0)]
200
200
 
201
201
 
202
202
  class Locale(Enum):
@@ -310,39 +310,30 @@ class Kind(BasicEnum):
310
310
  THUNDERY_SNOW_SHOWERS = 392
311
311
 
312
312
  @classmethod
313
- def _missing_(self, value: int) -> 'Kind':
314
- if value == 248 or value == 260:
313
+ def _missing_(self, value: int) -> Self:
314
+ if value in (248, 260):
315
315
  return self.FOG
316
- elif value == 263 or value == 353:
316
+ elif value in (263, 353):
317
317
  return self.LIGHT_SHOWERS
318
- elif value == 362 or value == 365 or value == 374:
318
+ elif value in (362, 365, 374):
319
319
  return self.LIGHT_SLEET_SHOWERS
320
- elif (
321
- value == 185
322
- or value == 281
323
- or value == 284
324
- or value == 311
325
- or value == 314
326
- or value == 317
327
- or value == 350
328
- or value == 377
329
- ):
320
+ elif value in (185, 281, 284, 311, 314, 317, 350, 377):
330
321
  return self.LIGHT_SLEET
331
322
  elif value == 386:
332
323
  return self.THUNDERY_SHOWERS
333
324
  elif value == 320:
334
325
  return self.LIGHT_SNOW
335
- elif value == 329 or value == 332 or value == 338:
326
+ elif value in (329, 332, 338):
336
327
  return self.HEAVY_SNOW
337
- elif value == 293 or value == 296:
328
+ elif value in (293, 296):
338
329
  return self.LIGHT_RAIN
339
- elif value == 305 or value == 356:
330
+ elif value in (305, 356):
340
331
  return self.HEAVY_SHOWERS
341
- elif value == 308 or value == 359:
332
+ elif value in (308, 359):
342
333
  return self.HEAVY_RAIN
343
- elif value == 326 or value == 368:
334
+ elif value in (326, 368):
344
335
  return self.LIGHT_SNOW_SHOWERS
345
- elif value == 371 or value == 395:
336
+ elif value in (371, 395):
346
337
  return self.HEAVY_SNOW_SHOWERS
347
338
 
348
339
  @property
python_weather/errors.py CHANGED
@@ -32,12 +32,12 @@ class Error(Exception):
32
32
 
33
33
 
34
34
  class RequestError(Error):
35
- """Thrown upon HTTP request failure. Extends :class:`Error`."""
35
+ """Thrown upon HTTP request failure. Extends :class:`~python_weather.errors.Error`."""
36
36
 
37
37
  __slots__: Tuple[str, ...] = ('source',)
38
38
 
39
39
  source: Exception
40
- """The :class:`Exception` instance causing this exception."""
40
+ """The :py:class:`Exception` instance causing this exception."""
41
41
 
42
42
  def __init__(self, source: Exception):
43
43
  self.source = source
@@ -25,7 +25,7 @@ SOFTWARE.
25
25
  from typing import Iterable, Optional, Tuple, List
26
26
  from datetime import datetime, date, time
27
27
 
28
- from .base import BaseForecast, CustomizableBase
28
+ from .base import BaseForecast
29
29
  from .enums import Phase, Locale, HeatIndex
30
30
  from .constants import _Unit, LATLON_REGEX
31
31
 
@@ -33,318 +33,264 @@ from .constants import _Unit, LATLON_REGEX
33
33
  class HourlyForecast(BaseForecast):
34
34
  """Represents a weather forecast of a specific hour."""
35
35
 
36
- __slots__: Tuple[str, ...] = ()
36
+ __slots__: Tuple[str, ...] = (
37
+ 'chances_of_fog',
38
+ 'chances_of_frost',
39
+ 'chances_of_high_temperature',
40
+ 'chances_of_overcast',
41
+ 'chances_of_rain',
42
+ 'chances_of_remaining_dry',
43
+ 'chances_of_snow',
44
+ 'chances_of_sunshine',
45
+ 'chances_of_thunder',
46
+ 'chances_of_windy',
47
+ 'cloud_cover',
48
+ 'time',
49
+ 'dew_point',
50
+ 'heat_index',
51
+ 'wind_chill',
52
+ 'wind_gust',
53
+ )
37
54
 
38
- def __init__(self, json: dict, unit: _Unit, locale: Locale):
39
- # for inheritance purposes
40
- if 'temp_C' not in json:
41
- json['temp_C'] = json.pop('tempC')
42
- if 'temp_F' not in json:
43
- json['temp_F'] = json.pop('tempF')
44
-
45
- super().__init__(json, unit, locale)
46
-
47
- def __repr__(self) -> str:
48
- return f'<{__class__.__name__} time={self.time!r} temperature={self.temperature!r} description={self.description!r} kind={self.kind!r}>'
49
-
50
- @property
51
- def dew_point(self) -> int:
52
- """The dew point in either celcius or fahrenheit."""
55
+ chances_of_fog: int
56
+ """Chances of a fog in percent."""
53
57
 
54
- return int(
55
- self._BaseForecast__inner[f'DewPoint{self._CustomizableBase__unit.temperature}']
56
- )
57
-
58
- @property
59
- def heat_index(self) -> HeatIndex:
60
- """The heat index in either celcius or fahrenheit."""
61
-
62
- celcius_index = int(self._BaseForecast__inner['HeatIndexC'])
63
-
64
- return HeatIndex._new(
65
- celcius_index,
66
- int(
67
- self._BaseForecast__inner[
68
- f'HeatIndex{self._CustomizableBase__unit.temperature}'
69
- ]
70
- ),
71
- )
72
-
73
- @property
74
- def wind_chill(self) -> int:
75
- """The wind chill value in either celcius or fahrenheit."""
76
-
77
- return int(
78
- self._BaseForecast__inner[f'WindChill{self._CustomizableBase__unit.temperature}']
79
- )
80
-
81
- @property
82
- def wind_gust(self) -> int:
83
- """The wind gust value in either kilometers/hour or miles/hour."""
84
-
85
- return int(
86
- self._BaseForecast__inner[f'WindGust{self._CustomizableBase__unit.velocity}']
87
- )
58
+ chances_of_frost: int
59
+ """Chances of a frost in percent."""
88
60
 
89
- @property
90
- def chances_of_fog(self) -> int:
91
- """Chances of a fog in percent."""
61
+ chances_of_high_temperature: int
62
+ """Chances of a high temperature in percent."""
92
63
 
93
- return int(self._BaseForecast__inner['chanceoffog'])
64
+ chances_of_overcast: int
65
+ """Chances of an overcast in percent."""
94
66
 
95
- @property
96
- def chances_of_frost(self) -> int:
97
- """Chances of a frost in percent."""
67
+ chances_of_rain: int
68
+ """Chances of a rain in percent."""
98
69
 
99
- return int(self._BaseForecast__inner['chanceoffrost'])
70
+ chances_of_remaining_dry: int
71
+ """Chances of remaining dry in percent."""
100
72
 
101
- @property
102
- def chances_of_high_temperature(self) -> int:
103
- """Chances of a high temperature in percent."""
73
+ chances_of_snow: int
74
+ """Chances of a snow in percent."""
104
75
 
105
- return int(self._BaseForecast__inner['chanceofhightemp'])
76
+ chances_of_sunshine: int
77
+ """Chances of a sunshine in percent."""
106
78
 
107
- @property
108
- def chances_of_overcast(self) -> int:
109
- """Chances of an overcast in percent."""
79
+ chances_of_thunder: int
80
+ """Chances of a thunder in percent."""
110
81
 
111
- return int(self._BaseForecast__inner['chanceofovercast'])
82
+ chances_of_windy: int
83
+ """Chances of windy in percent."""
112
84
 
113
- @property
114
- def chances_of_rain(self) -> int:
115
- """Chances of a rain in percent."""
85
+ cloud_cover: int
86
+ """The cloud cover value in percent."""
116
87
 
117
- return int(self._BaseForecast__inner['chanceofrain'])
88
+ time: 'time'
89
+ """The local time in hours and minutes."""
118
90
 
119
- @property
120
- def chances_of_remaining_dry(self) -> int:
121
- """Chances of remaining dry in percent."""
91
+ dew_point: int
92
+ """The dew point in either celcius or fahrenheit."""
122
93
 
123
- return int(self._BaseForecast__inner['chanceofremdry'])
94
+ heat_index: HeatIndex
95
+ """The heat index in either celcius or fahrenheit."""
124
96
 
125
- @property
126
- def chances_of_snow(self) -> int:
127
- """Chances of a snow in percent."""
97
+ wind_chill: int
98
+ """The wind chill value in either celcius or fahrenheit."""
128
99
 
129
- return int(self._BaseForecast__inner['chanceofsnow'])
100
+ wind_gust: int
101
+ """The wind gust value in either kilometers/hour or miles/hour."""
130
102
 
131
- @property
132
- def chances_of_sunshine(self) -> int:
133
- """Chances of a sunshine in percent."""
134
-
135
- return int(self._BaseForecast__inner['chanceofsunshine'])
136
-
137
- @property
138
- def chances_of_thunder(self) -> int:
139
- """Chances of a thunder in percent."""
140
-
141
- return int(self._BaseForecast__inner['chanceofthunder'])
142
-
143
- @property
144
- def chances_of_windy(self) -> int:
145
- """Chances of windy in percent."""
146
-
147
- return int(self._BaseForecast__inner['chanceofwindy'])
148
-
149
- @property
150
- def cloud_cover(self) -> int:
151
- """The cloud cover value in percent."""
152
-
153
- return int(self._BaseForecast__inner['cloudcover'])
154
-
155
- @property
156
- def time(self) -> time:
157
- """The local time in hours and minutes."""
103
+ def __init__(self, json: dict, unit: _Unit, locale: Locale):
104
+ # for inheritance purposes
105
+ if 'temp_C' not in json:
106
+ json['temp_C'] = json.pop('tempC')
107
+ if 'temp_F' not in json:
108
+ json['temp_F'] = json.pop('tempF')
158
109
 
159
- return (
160
- time()
161
- if len(self._BaseForecast__inner['time']) < 3
162
- else datetime.strptime(self._BaseForecast__inner['time'], '%H%M').time()
110
+ celcius_index = int(json['HeatIndexC'])
111
+ t = json['time']
112
+
113
+ self.chances_of_fog = int(json['chanceoffog'])
114
+ self.chances_of_frost = int(json['chanceoffrost'])
115
+ self.chances_of_high_temperature = int(json['chanceofhightemp'])
116
+ self.chances_of_overcast = int(json['chanceofovercast'])
117
+ self.chances_of_rain = int(json['chanceofrain'])
118
+ self.chances_of_remaining_dry = int(json['chanceofremdry'])
119
+ self.chances_of_snow = int(json['chanceofsnow'])
120
+ self.chances_of_sunshine = int(json['chanceofsunshine'])
121
+ self.chances_of_thunder = int(json['chanceofthunder'])
122
+ self.chances_of_windy = int(json['chanceofwindy'])
123
+ self.cloud_cover = int(json['cloudcover'])
124
+ self.time = time() if len(t) < 3 else datetime.strptime(t, '%H%M').time()
125
+ self.dew_point = int(json[f'DewPoint{unit.temperature}'])
126
+ self.heat_index = HeatIndex._new(
127
+ celcius_index,
128
+ int(json[f'HeatIndex{unit.temperature}']),
163
129
  )
130
+ self.wind_chill = int(json[f'WindChill{unit.temperature}'])
131
+ self.wind_gust = int(json[f'WindGust{unit.velocity}'])
164
132
 
133
+ super().__init__(json, unit, locale)
165
134
 
166
- class DailyForecast(CustomizableBase):
167
- __slots__: Tuple[str, ...] = ('__inner', '__astronomy')
168
-
169
- def __init__(self, json: dict, unit: _Unit, locale: Locale):
170
- self.__astronomy = json.pop('astronomy')[0]
171
- self.__inner = json
135
+ def __repr__(self) -> str:
136
+ return f'<{__class__.__name__} time={self.time!r} temperature={self.temperature!r} description={self.description!r} kind={self.kind!r}>'
172
137
 
173
- super().__init__(unit, locale)
174
138
 
175
- def __repr__(self) -> str:
176
- return f'<{__class__.__name__} date={self.date!r} temperature={self.temperature!r}>'
139
+ class DailyForecast:
140
+ __slots__: Tuple[str, ...] = (
141
+ 'moon_illumination',
142
+ 'moon_phase',
143
+ 'moonrise',
144
+ 'moonset',
145
+ 'sunrise',
146
+ 'sunset',
147
+ 'date',
148
+ 'sunlight',
149
+ 'lowest_temperature',
150
+ 'highest_temperature',
151
+ 'temperature',
152
+ 'snowfall',
153
+ 'hourly_forecasts',
154
+ )
177
155
 
178
- def __iter__(self) -> Iterable[HourlyForecast]:
179
- return self.hourly_forecasts
156
+ moon_illumination: int
157
+ """The percentage of the moon illuminated."""
180
158
 
181
- def __list__(self) -> List[HourlyForecast]:
182
- return list(iter(self))
159
+ moon_phase: Phase
160
+ """The moon's phase."""
183
161
 
184
- @property
185
- def moon_illumination(self) -> int:
186
- """The percentage of the moon illuminated."""
162
+ moonrise: Optional[time]
163
+ """The local time when the moon rises. This can be ``None``."""
187
164
 
188
- return int(self.__astronomy['moon_illumination'])
165
+ moonset: Optional[time]
166
+ """The local time when the moon sets. This can be ``None``."""
189
167
 
190
- @property
191
- def moon_phase(self) -> Phase:
192
- """The moon's phase."""
168
+ sunrise: Optional[time]
169
+ """The local time when the sun rises. This can be ``None``."""
193
170
 
194
- return Phase(self.__astronomy['moon_phase'])
171
+ sunset: Optional[time]
172
+ """The local time when the sun sets. This can be ``None``."""
195
173
 
196
- @property
197
- def moonrise(self) -> Optional[time]:
198
- """The local time when the moon rises. This can be ``None``."""
174
+ date: 'date'
175
+ """The local date of this forecast."""
199
176
 
200
- try:
201
- return datetime.strptime(self.__astronomy['moonrise'], '%I:%M %p').time()
202
- except ValueError:
203
- ...
177
+ sunlight: float
178
+ """Hours of sunlight."""
204
179
 
205
- @property
206
- def moonset(self) -> Optional[time]:
207
- """The local time when the moon sets. This can be ``None``."""
180
+ lowest_temperature: int
181
+ """The lowest temperature in either celcius or fahrenheit."""
208
182
 
209
- try:
210
- return datetime.strptime(self.__astronomy['moonset'], '%I:%M %p').time()
211
- except ValueError:
212
- ...
183
+ highest_temperature: int
184
+ """The highest temperature in either celcius or fahrenheit."""
213
185
 
214
- @property
215
- def sunrise(self) -> Optional[time]:
216
- """The local time when the sun rises. This can be ``None``."""
186
+ temperature: int
187
+ """The average temperature in either celcius or fahrenheit."""
217
188
 
218
- try:
219
- return datetime.strptime(self.__astronomy['sunrise'], '%I:%M %p').time()
220
- except ValueError:
221
- ...
189
+ snowfall: float
190
+ """Total snowfall in either centimeters or inches."""
222
191
 
223
- @property
224
- def sunset(self) -> Optional[time]:
225
- """The local time when the sun sets. This can be ``None``."""
192
+ hourly_forecasts: List[HourlyForecast]
193
+ """The hourly forecasts of this day."""
226
194
 
195
+ def __init__(self, json: dict, unit: _Unit, locale: Locale):
196
+ astronomy = json['astronomy'][0]
197
+
198
+ self.moon_illumination = int(astronomy['moon_illumination'])
199
+ self.moon_phase = Phase(astronomy['moon_phase'])
200
+ self.moonrise = __class__.__parse_time(astronomy['moonrise'])
201
+ self.moonset = __class__.__parse_time(astronomy['moonset'])
202
+ self.sunrise = __class__.__parse_time(astronomy['sunrise'])
203
+ self.sunset = __class__.__parse_time(astronomy['sunset'])
204
+ self.date = datetime.strptime(json['date'], '%Y-%m-%d').date()
205
+ self.sunlight = float(json['sunHour'])
206
+ self.lowest_temperature = int(json[f'mintemp{unit.temperature}'])
207
+ self.highest_temperature = int(json[f'maxtemp{unit.temperature}'])
208
+ self.temperature = int(json[f'avgtemp{unit.temperature}'])
209
+ self.snowfall = float(json['totalSnow_cm']) / unit.cm_divisor
210
+ self.hourly_forecasts = [
211
+ HourlyForecast(elem, unit, locale) for elem in json['hourly']
212
+ ]
213
+
214
+ @staticmethod
215
+ def __parse_time(timestamp: str) -> Optional[time]:
227
216
  try:
228
- return datetime.strptime(self.__astronomy['sunset'], '%I:%M %p').time()
217
+ return datetime.strptime(timestamp, '%I:%M %p').time()
229
218
  except ValueError:
230
219
  ...
231
220
 
232
- @property
233
- def date(self) -> date:
234
- """The local date of this forecast."""
221
+ def __repr__(self) -> str:
222
+ return f'<{__class__.__name__} date={self.date!r} temperature={self.temperature!r}>'
235
223
 
236
- return datetime.strptime(self.__inner['date'], '%Y-%m-%d').date()
224
+ def __len__(self) -> int:
225
+ return len(self.hourly_forecasts)
237
226
 
238
- @property
239
- def lowest_temperature(self) -> int:
240
- """The lowest temperature in either celcius or fahrenheit."""
227
+ def __iter__(self) -> Iterable[HourlyForecast]:
228
+ return iter(self.hourly_forecasts)
241
229
 
242
- return int(self.__inner[f'mintemp{self._CustomizableBase__unit.temperature}'])
243
230
 
244
- @property
245
- def highest_temperature(self) -> int:
246
- """The highest temperature in either celcius or fahrenheit."""
231
+ class Forecast(BaseForecast):
232
+ """Represents today's weather forecast, alongside daily and hourly weather forecasts."""
247
233
 
248
- return int(self.__inner[f'maxtemp{self._CustomizableBase__unit.temperature}'])
234
+ __slots__: Tuple[str, ...] = (
235
+ 'local_population',
236
+ 'region',
237
+ 'location',
238
+ 'country',
239
+ 'datetime',
240
+ 'coordinates',
241
+ 'daily_forecasts',
242
+ )
249
243
 
250
- @property
251
- def temperature(self) -> int:
252
- """The average temperature in either celcius or fahrenheit."""
244
+ local_population: int
245
+ """The local population count."""
253
246
 
254
- return int(self.__inner[f'avgtemp{self._CustomizableBase__unit.temperature}'])
247
+ region: str
248
+ """The local region's name."""
255
249
 
256
- @property
257
- def sunlight(self) -> float:
258
- """Hours of sunlight."""
250
+ location: str
251
+ """The location's name."""
259
252
 
260
- return float(self.__inner['sunHour'])
253
+ country: str
254
+ """The local country's name."""
261
255
 
262
- @property
263
- def snowfall(self) -> float:
264
- """Total snowfall in either centimeters or inches."""
256
+ datetime: 'datetime'
257
+ """The local date and time of this weather forecast."""
265
258
 
266
- return float(self.__inner['totalSnow_cm']) / self._CustomizableBase__unit.cm_divisor
259
+ coordinates: Tuple[float, float]
260
+ """A tuple of this forecast's latitude and longitude."""
267
261
 
268
- @property
269
- def hourly_forecasts(self) -> Iterable[HourlyForecast]:
270
- """The hourly forecasts of this day."""
262
+ daily_forecasts: List[DailyForecast]
263
+ """Daily weather forecasts in this location."""
271
264
 
272
- return (
273
- HourlyForecast(elem, self._CustomizableBase__unit, self._CustomizableBase__locale)
274
- for elem in self.__inner['hourly']
275
- )
265
+ def __init__(self, json: dict, unit: _Unit, locale: Locale):
266
+ current = json['current_condition'][0]
267
+ nearest = json['nearest_area'][0]
276
268
 
269
+ self.local_population = int(nearest['population'])
270
+ self.region = nearest['region'][0]['value']
271
+ self.location = nearest['areaName'][0]['value']
272
+ self.country = nearest['country'][0]['value']
273
+ self.datetime = datetime.strptime(current['localObsDateTime'], '%Y-%m-%d %I:%M %p')
277
274
 
278
- class Forecast(BaseForecast):
279
- """Represents today's weather forecast, alongside daily and hourly weather forecasts."""
275
+ try:
276
+ req = next(filter(lambda x: x['type'] == 'LatLon', json['request']))
277
+ match = LATLON_REGEX.match(req['query'])
280
278
 
281
- __slots__: Tuple[str, ...] = ('__inner', '__nearest')
279
+ self.coordinates = (float(match[1]), float(match[2]))
280
+ except:
281
+ self.coordinates = (float(nearest['latitude']), float(nearest['longitude']))
282
282
 
283
- def __init__(self, json: dict, unit: _Unit, locale: Locale):
284
- current = json['current_condition'][0]
285
- self.__nearest = json.pop('nearest_area')[0]
286
- self.__inner = json
283
+ self.daily_forecasts = [
284
+ DailyForecast(elem, unit, locale) for elem in json['weather']
285
+ ]
287
286
 
288
287
  super().__init__(current, unit, locale)
289
288
 
290
289
  def __repr__(self) -> str:
291
290
  return f'<{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature!r}>'
292
291
 
293
- def __iter__(self) -> Iterable[DailyForecast]:
294
- return self.daily_forecasts
295
-
296
- def __list__(self) -> List[DailyForecast]:
297
- return list(iter(self))
298
-
299
- @property
300
- def local_population(self) -> int:
301
- """The local population count."""
302
-
303
- return int(self.__nearest['population'])
304
-
305
- @property
306
- def region(self) -> str:
307
- """The local region's name."""
308
-
309
- return self.__nearest['region'][0]['value']
310
-
311
- @property
312
- def location(self) -> str:
313
- """The location's name."""
314
-
315
- return self.__nearest['areaName'][0]['value']
316
-
317
- @property
318
- def country(self) -> str:
319
- """The local country's name."""
320
-
321
- return self.__nearest['country'][0]['value']
322
-
323
- @property
324
- def datetime(self) -> datetime:
325
- """The local date and time of this weather forecast."""
292
+ def __len__(self) -> int:
293
+ return len(self.daily_forecasts)
326
294
 
327
- return datetime.strptime(
328
- self._BaseForecast__inner['localObsDateTime'], '%Y-%m-%d %I:%M %p'
329
- )
330
-
331
- @property
332
- def daily_forecasts(self) -> Iterable[DailyForecast]:
333
- """Daily weather forecasts in this location."""
334
-
335
- return (
336
- DailyForecast(elem, self._CustomizableBase__unit, self._CustomizableBase__locale)
337
- for elem in self.__inner['weather']
338
- )
339
-
340
- @property
341
- def coordinates(self) -> Tuple[float, float]:
342
- """A tuple of this forecast's latitude and longitude."""
343
-
344
- try:
345
- req = next(filter(lambda x: x['type'] == 'LatLon', self.__inner['request']))
346
- match = LATLON_REGEX.match(req['query'])
347
-
348
- return float(match[1]), float(match[2])
349
- except:
350
- return float(self.__nearest['latitude']), float(self.__nearest['longitude'])
295
+ def __iter__(self) -> Iterable[DailyForecast]:
296
+ return iter(self.daily_forecasts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-weather
3
- Version: 2.0.4
3
+ Version: 2.0.6
4
4
  Summary: A free and asynchronous weather API wrapper made in Python, for Python.
5
5
  Author: null8626
6
6
  License: MIT
@@ -11,12 +11,11 @@ Project-URL: Donate via GitHub Sponsors, https://github.com/sponsors/null8626
11
11
  Project-URL: Donate via Ko-fi, https://ko-fi.com/null8626
12
12
  Keywords: weather,forecast,weather-api,weather-forecast
13
13
  Classifier: Development Status :: 5 - Production/Stable
14
- Classifier: Framework :: aiohttp
15
- Classifier: Framework :: AsyncIO
16
14
  Classifier: Intended Audience :: Education
17
15
  Classifier: Intended Audience :: Developers
18
16
  Classifier: Intended Audience :: Information Technology
19
17
  Classifier: Natural Language :: English
18
+ Classifier: Operating System :: OS Independent
20
19
  Classifier: Topic :: Education
21
20
  Classifier: Topic :: Internet
22
21
  Classifier: Topic :: Scientific/Engineering
@@ -32,10 +31,11 @@ Classifier: Programming Language :: Python :: 3.9
32
31
  Classifier: Programming Language :: Python :: 3.10
33
32
  Classifier: Programming Language :: Python :: 3.11
34
33
  Classifier: Programming Language :: Python :: 3.12
34
+ Classifier: Programming Language :: Python :: 3.13
35
35
  Requires-Python: >=3.8
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
- Requires-Dist: aiohttp >=3.10.8
38
+ Requires-Dist: aiohttp >=3.10.10
39
39
 
40
40
  # [python-weather][pypi-url] [![pypi][pypi-image]][pypi-url] [![downloads][downloads-image]][pypi-url] [![codacy-badge][codacy-url]][codacy-image] [![ko-fi][ko-fi-brief-image]][ko-fi-url]
41
41
 
@@ -0,0 +1,12 @@
1
+ python_weather/__init__.py,sha256=2MqLVKS6sre_Q-2YqV-gNDpJkbT5G2rAGGiz4JgeQ24,1586
2
+ python_weather/base.py,sha256=LU_Es_vMWKiptMeLV2CClmY-GEm54J528g8LfHxw0yw,4684
3
+ python_weather/client.py,sha256=w2tp8Mip2FFVAgq9fIMozjpVwptBLihSo04C2-UwDHw,5567
4
+ python_weather/constants.py,sha256=NzfQkxmAPEYAbUHMBUEi9-4XOq0z8FYj1_G_1z5Of-4,2015
5
+ python_weather/enums.py,sha256=9lMyxQjJBHbAu_ceX4maunbDCk4AWjbT8vTUgpBMsVQ,10375
6
+ python_weather/errors.py,sha256=VmRYlHx6s1SMLZf8J7fEkp64RbEAxGibqW9ZxkQdx6c,1600
7
+ python_weather/forecast.py,sha256=gNdvMisnjIiNyQQfgImltDPhgB6900PTeCl87hZ_gkI,9124
8
+ python_weather-2.0.6.dist-info/LICENSE,sha256=OhQiuzPzGigVn4yKdwlDJMfyglio438lXJU5ny4WCGk,1080
9
+ python_weather-2.0.6.dist-info/METADATA,sha256=xCzycdBmD5XDMyAiQhOrppFO8RmHA8tZ4w8YU4tuhwM,4130
10
+ python_weather-2.0.6.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
11
+ python_weather-2.0.6.dist-info/top_level.txt,sha256=NDqLSO3dHB1tjjiNJfEg-a9_eoOmawAZynZsy9Yu48w,15
12
+ python_weather-2.0.6.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- python_weather/__init__.py,sha256=FbHn9_8A63GXbpFmiyPalMPynvtcurGVX8zZk96VNZs,1586
2
- python_weather/base.py,sha256=esxcbl0nf7ExIczzG0cpvTKAFFKEKRtprj5sU_6wBqQ,5031
3
- python_weather/client.py,sha256=k9PXBUhQIGOLluNasoOpMhvKMblOfGqZOnGE1lDU_Is,5026
4
- python_weather/constants.py,sha256=NzfQkxmAPEYAbUHMBUEi9-4XOq0z8FYj1_G_1z5Of-4,2015
5
- python_weather/enums.py,sha256=e5O1rbyQD42vv4ATDJP4z32UPbPn5UVytNdtycYhBOE,10650
6
- python_weather/errors.py,sha256=sbCEmzXvC_jxyCVJ8aBVOwIMU3FSM4d0Myu2rYUWaI8,1574
7
- python_weather/forecast.py,sha256=iFoGBhrVELHzbsuJsHcH9xM5FHcbQF8JiRhR74b4yfo,10101
8
- python_weather-2.0.4.dist-info/LICENSE,sha256=OhQiuzPzGigVn4yKdwlDJMfyglio438lXJU5ny4WCGk,1080
9
- python_weather-2.0.4.dist-info/METADATA,sha256=1IgvugBGriUP0sYZIePFw1Mo4WgnGvOBYbq_Kl6iUjQ,4097
10
- python_weather-2.0.4.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
11
- python_weather-2.0.4.dist-info/top_level.txt,sha256=NDqLSO3dHB1tjjiNJfEg-a9_eoOmawAZynZsy9Yu48w,15
12
- python_weather-2.0.4.dist-info/RECORD,,