python-weather 2.1.2__tar.gz → 2.2.0__tar.gz

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.
Files changed (20) hide show
  1. {python_weather-2.1.2/python_weather.egg-info → python_weather-2.2.0}/PKG-INFO +3 -3
  2. {python_weather-2.1.2 → python_weather-2.2.0}/pyproject.toml +3 -3
  3. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/base.py +2 -55
  4. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/client.py +64 -13
  5. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/constants.py +3 -0
  6. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/enums.py +24 -8
  7. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/errors.py +1 -0
  8. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/forecast.py +7 -0
  9. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/version.py +1 -1
  10. {python_weather-2.1.2 → python_weather-2.2.0/python_weather.egg-info}/PKG-INFO +3 -3
  11. python_weather-2.2.0/python_weather.egg-info/requires.txt +1 -0
  12. python_weather-2.1.2/python_weather.egg-info/requires.txt +0 -1
  13. {python_weather-2.1.2 → python_weather-2.2.0}/LICENSE +0 -0
  14. {python_weather-2.1.2 → python_weather-2.2.0}/MANIFEST.in +0 -0
  15. {python_weather-2.1.2 → python_weather-2.2.0}/README.md +0 -0
  16. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather/__init__.py +0 -0
  17. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather.egg-info/SOURCES.txt +0 -0
  18. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather.egg-info/dependency_links.txt +0 -0
  19. {python_weather-2.1.2 → python_weather-2.2.0}/python_weather.egg-info/top_level.txt +0 -0
  20. {python_weather-2.1.2 → python_weather-2.2.0}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-weather
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: A free and asynchronous weather API wrapper made in Python, for Python.
5
5
  Author: null8626
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Documentation, https://python-weather.readthedocs.io/en/latest/
8
8
  Project-URL: Repository, https://github.com/null8626/python-weather
9
9
  Project-URL: Changelog, https://python-weather.readthedocs.io/en/latest/changelog.html
@@ -33,7 +33,7 @@ Classifier: Programming Language :: Python :: 3.14
33
33
  Requires-Python: >=3.10
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: aiohttp>=3.13.3
36
+ Requires-Dist: aiohttp>=3.13.5
37
37
  Dynamic: license-file
38
38
 
39
39
  # [python-weather][pypi-url] [![pypi][pypi-image]][pypi-url] [![pypi downloads][pypi-downloads-image]][pypi-url] [![codacy-badge][codacy-image]][codacy-url] [![codecov-badge][codecov-image]][codecov-url] [![ko-fi][ko-fi-brief-image]][ko-fi-url]
@@ -3,13 +3,13 @@ requires = ["setuptools"]
3
3
 
4
4
  [project]
5
5
  name = "python-weather"
6
- version = "2.1.2"
6
+ version = "2.2.0"
7
7
  description = "A free and asynchronous weather API wrapper made in Python, for Python."
8
8
  readme = "README.md"
9
- license = { text = "MIT" }
9
+ license = "MIT"
10
10
  authors = [{ name = "null8626" }]
11
11
  keywords = ["weather", "forecast", "weather-forecast"]
12
- dependencies = ["aiohttp>=3.13.3"]
12
+ dependencies = ["aiohttp>=3.13.5"]
13
13
  classifiers = [
14
14
  "Development Status :: 5 - Production/Stable",
15
15
  "Intended Audience :: Education",
@@ -3,64 +3,11 @@
3
3
 
4
4
  from .enums import WindDirection, Kind, Locale, UltraViolet
5
5
  from .constants import _Unit
6
- from .errors import Error
7
-
8
-
9
- class CustomizableBase:
10
- __slots__: tuple[str, ...] = '_unit', '_locale'
11
-
12
- _unit: _Unit
13
- _locale: Locale
14
-
15
- def __init__(self, unit: _Unit, locale: Locale):
16
- self.unit = unit
17
- self.locale = locale
18
-
19
- @property
20
- def unit(self) -> _Unit:
21
- """The measuring unit used."""
22
-
23
- return self._unit
24
-
25
- @unit.setter
26
- def unit(self, to: _Unit) -> None:
27
- """
28
- Sets the default measuring unit used.
29
-
30
- :param to: The new default measuring unit to be used.
31
-
32
- :exception Error: ``to`` is not either :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL`.
33
- """
34
-
35
- if not isinstance(to, _Unit):
36
- raise Error('Invalid measuring unit specified!')
37
-
38
- self._unit = to
39
-
40
- @property
41
- def locale(self) -> Locale:
42
- """The localization used."""
43
-
44
- return self._locale
45
-
46
- @locale.setter
47
- def locale(self, to: Locale) -> None:
48
- """
49
- Sets the default localization used.
50
-
51
- :param to: The new :class:`.Locale` to be used.
52
- :type to: :class:`.Locale`
53
-
54
- :exception Error: ``to`` is not a part of the :class:`.Locale` enum.
55
- """
56
-
57
- if not isinstance(to, Locale):
58
- raise Error(f'Expected {to!r} to be a Locale enum')
59
-
60
- self._locale = to
61
6
 
62
7
 
63
8
  class BaseForecast:
9
+ """A base weather forecast."""
10
+
64
11
  __slots__: tuple[str, ...] = (
65
12
  'cloud_cover',
66
13
  'ultraviolet',
@@ -7,13 +7,12 @@ from asyncio import sleep
7
7
 
8
8
  from .errors import Error, RequestError
9
9
  from .constants import _Unit, METRIC
10
- from .base import CustomizableBase
11
10
  from .forecast import Forecast
12
11
  from .version import VERSION
13
12
  from .enums import Locale
14
13
 
15
14
 
16
- class Client(CustomizableBase):
15
+ class Client:
17
16
  """
18
17
  Interact with the API's endpoints.
19
18
 
@@ -45,11 +44,19 @@ class Client(CustomizableBase):
45
44
  :exception Error: ``unit`` is not :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL` or ``locale`` is not a part of the :class:`.Locale` enum.
46
45
  """
47
46
 
48
- __slots__: tuple[str, ...] = '__own_session', '__session', '_max_retries'
47
+ __slots__: tuple[str, ...] = (
48
+ '__own_session',
49
+ '__session',
50
+ '_max_retries',
51
+ '_unit',
52
+ '_locale',
53
+ )
49
54
 
50
55
  __own_session: bool
51
56
  __session: ClientSession
52
57
  _max_retries: int
58
+ _unit: _Unit
59
+ _locale: Locale
53
60
 
54
61
  def __init__(
55
62
  self,
@@ -59,18 +66,58 @@ class Client(CustomizableBase):
59
66
  session: ClientSession | None = None,
60
67
  max_retries: int | None = None,
61
68
  ):
62
- super().__init__(unit, locale)
63
-
64
69
  self.__own_session = session is None
65
70
  self.__session = session or ClientSession(
66
71
  timeout=ClientTimeout(total=5000.0),
67
72
  connector=TCPConnector(ssl=False),
68
73
  )
69
74
  self._max_retries = max_retries or 3
75
+ self.unit = unit
76
+ self.locale = locale
70
77
 
71
78
  def __repr__(self) -> str:
79
+ """The client's debug string representation."""
72
80
  return f'<{__class__.__module__}.{__class__.__name__} {self.__session!r}>'
73
81
 
82
+ @property
83
+ def unit(self) -> _Unit:
84
+ """The measuring unit used."""
85
+ return self._unit
86
+
87
+ @unit.setter
88
+ def unit(self, to: _Unit) -> None:
89
+ """
90
+ Sets the default measuring unit used.
91
+
92
+ :param to: The new default measuring unit to be used.
93
+
94
+ :exception Error: ``to`` is not either :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL`.
95
+ """
96
+ if not isinstance(to, _Unit):
97
+ raise Error('Invalid measuring unit specified!')
98
+
99
+ self._unit = to
100
+
101
+ @property
102
+ def locale(self) -> Locale:
103
+ """The localization used."""
104
+ return self._locale
105
+
106
+ @locale.setter
107
+ def locale(self, to: Locale) -> None:
108
+ """
109
+ Sets the default localization used.
110
+
111
+ :param to: The new :class:`.Locale` to be used.
112
+ :type to: :class:`.Locale`
113
+
114
+ :exception Error: ``to`` is not a part of the :class:`.Locale` enum.
115
+ """
116
+ if not isinstance(to, Locale):
117
+ raise Error(f'Expected {to!r} to be a Locale enum')
118
+
119
+ self._locale = to
120
+
74
121
  async def get(
75
122
  self,
76
123
  location: str,
@@ -94,19 +141,20 @@ class Client(CustomizableBase):
94
141
  :param locale: Overrides the locale used.
95
142
  :type locale: :class:`.Locale` | :py:obj:`None`
96
143
 
97
- :exception TypeError: ``location`` is not a :py:class:`str` or is empty.
144
+ :exception TypeError: The specified location is not a string.
145
+ :exception ValueError: The specified location is empty.
98
146
  :exception Error: The client is already closed.
99
147
  :exception RequestError: The client received a non-favorable response from the API.
100
148
 
101
149
  :returns: The requested weather forecast.
102
150
  :rtype: Forecast
103
151
  """
104
-
105
152
  if self.__session.closed:
106
153
  raise Error('Client session is already closed.')
107
-
108
- elif not isinstance(location, str) or not location:
109
- raise TypeError(f'Expected a proper location str, got {location!r}')
154
+ elif not isinstance(location, str):
155
+ raise TypeError('The specified location must be a string.')
156
+ elif not location:
157
+ raise ValueError('The specified location must not be empty.')
110
158
 
111
159
  if not isinstance(unit, _Unit):
112
160
  unit = self._unit
@@ -134,7 +182,9 @@ class Client(CustomizableBase):
134
182
 
135
183
  resp.raise_for_status()
136
184
 
137
- return Forecast(await resp.json(), unit, locale)
185
+ return Forecast(
186
+ await resp.json(content_type='application/text'), unit, locale
187
+ )
138
188
  except ClientResponseError:
139
189
  if attempts == self._max_retries:
140
190
  raise RequestError(status, reason) from None
@@ -144,7 +194,7 @@ class Client(CustomizableBase):
144
194
 
145
195
  async def close(self) -> None:
146
196
  """
147
- Closes the :class:`.Client` object.
197
+ Closes the client.
148
198
 
149
199
  Example:
150
200
 
@@ -152,12 +202,13 @@ class Client(CustomizableBase):
152
202
 
153
203
  await client.close()
154
204
  """
155
-
156
205
  if self.__own_session and not self.__session.closed:
157
206
  await self.__session.close()
158
207
 
159
208
  async def __aenter__(self) -> 'Client':
209
+ """Starts using the client. This method is no-op and just returns itself."""
160
210
  return self
161
211
 
162
212
  async def __aexit__(self, *_, **__) -> None:
213
+ """Closes the client."""
163
214
  await self.close()
@@ -5,6 +5,8 @@ import re
5
5
 
6
6
 
7
7
  class _Unit:
8
+ """A supported measurement unit."""
9
+
8
10
  __slots__: tuple[str, ...] = (
9
11
  'temperature',
10
12
  'velocity',
@@ -38,6 +40,7 @@ class _Unit:
38
40
  self.cm_divisor = cm_divisor
39
41
 
40
42
  def __repr__(self) -> str:
43
+ """The unit's debug string representation."""
41
44
  return f'<Unit [{self.temperature}, {self.velocity}]>'
42
45
 
43
46
 
@@ -7,12 +7,16 @@ from .constants import KIND_EMOJIS, WIND_DIRECTION_EMOJIS
7
7
 
8
8
 
9
9
  class BasicEnum(Enum):
10
+ """An ordinary enum."""
11
+
10
12
  __slots__: tuple[str, ...] = ()
11
13
 
12
14
  def __repr__(self) -> str:
15
+ """The enum's debug string representation."""
13
16
  return f'{self.__class__.__name__}.{self.name}'
14
17
 
15
18
  def __str__(self) -> str:
19
+ """The enum's friendly name."""
16
20
  return self.name.replace('_', ' ').title()
17
21
 
18
22
  @property
@@ -21,36 +25,46 @@ class BasicEnum(Enum):
21
25
 
22
26
 
23
27
  class IndexedEnum(Enum):
28
+ """An enum that carries an index value."""
29
+
24
30
  __slots__: tuple[str, ...] = ('index',)
25
31
 
26
32
  index: int
27
33
  """The index value."""
28
34
 
29
35
  def __lt__(self, other: 'IndexedEnum | float | int') -> bool:
36
+ """Checks if the enum's index value is less than the other."""
30
37
  return float(self.index) < float(other)
31
38
 
32
39
  def __le__(self, other: 'IndexedEnum | float | int') -> bool:
40
+ """Checks if the enum's index value is less than or equal to the other."""
33
41
  return float(self.index) <= float(other)
34
42
 
35
43
  def __eq__(self, other: object) -> bool:
44
+ """Checks if the enum's index value is equal to the other."""
36
45
  if other_float := getattr(other, '__float__', None):
37
46
  return float(self.index) == other_float()
38
47
 
39
48
  return False # pragma: nocover
40
49
 
41
50
  def __gt__(self, other: 'IndexedEnum | float | int') -> bool:
51
+ """Checks if the enum's index value is greater than the other."""
42
52
  return float(self.index) > float(other)
43
53
 
44
54
  def __ge__(self, other: 'IndexedEnum | float | int') -> bool:
55
+ """Checks if the enum's index value is greater than or equal to the other."""
45
56
  return float(self.index) >= float(other)
46
57
 
47
58
  def __hash__(self) -> int:
59
+ """The enum's index value."""
48
60
  return self.index
49
61
 
50
62
  def __int__(self) -> int:
63
+ """The enum's index value."""
51
64
  return self.index
52
65
 
53
66
  def __float__(self) -> float:
67
+ """The floating point representation of the enum's index value."""
54
68
  return float(self.index)
55
69
 
56
70
 
@@ -140,7 +154,7 @@ class WindDirection(BasicEnum):
140
154
  NORTH_NORTHWEST = 'NNW'
141
155
 
142
156
  degrees: float
143
- """The wind direction's value in degrees."""
157
+ """The wind direction's angle in degrees."""
144
158
 
145
159
  @staticmethod
146
160
  def _new(value: str, degrees: float) -> 'WindDirection':
@@ -150,25 +164,27 @@ class WindDirection(BasicEnum):
150
164
  return enum
151
165
 
152
166
  def __contains__(self, other: 'WindDirection | float | int') -> bool:
167
+ """Checks if the other's angle is within the enum's wind direction."""
153
168
  members = list(self.__class__)
154
169
 
155
170
  return self is members[int(((float(other) % 360) + 11.25) // 22.5) % len(members)]
156
171
 
157
172
  def __int__(self) -> int:
173
+ """The integer representation of the wind direction's angle."""
158
174
  return int(self.degrees)
159
175
 
160
176
  def __float__(self) -> float:
177
+ """The wind direction's angle."""
161
178
  return self.degrees
162
179
 
163
180
  @property
164
181
  def emoji(self) -> str:
165
- """Emoji representation."""
166
-
182
+ """The wind direction's emoji representation."""
167
183
  return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360) // 45)]
168
184
 
169
185
 
170
186
  class Locale(Enum):
171
- """Supported locales/languages."""
187
+ """A supported locale."""
172
188
 
173
189
  __slots__: tuple[str, ...] = ()
174
190
 
@@ -246,9 +262,11 @@ class Locale(Enum):
246
262
  ZULU = 'zu'
247
263
 
248
264
  def __repr__(self) -> str:
265
+ """The locale's debug string representation."""
249
266
  return f'{__class__.__name__}.{self.name}'
250
267
 
251
268
  def __str__(self) -> str:
269
+ """The locale's friendly name."""
252
270
  arr = self.name.title().split('_')
253
271
 
254
272
  return f'{" ".join(arr[:-1])} ({arr[-1]})' if len(arr) != 1 else arr[0]
@@ -307,8 +325,7 @@ class Kind(BasicEnum):
307
325
 
308
326
  @property
309
327
  def emoji(self) -> str:
310
- """Emoji representation."""
311
-
328
+ """The weather forecast kind's emoji representation."""
312
329
  return KIND_EMOJIS[self._index]
313
330
 
314
331
 
@@ -328,6 +345,5 @@ class Phase(BasicEnum):
328
345
 
329
346
  @property
330
347
  def emoji(self) -> str:
331
- """Emoji representation."""
332
-
348
+ """The moon phase's emoji representation."""
333
349
  return chr(0x1F311 + self._index)
@@ -26,4 +26,5 @@ class RequestError(Error):
26
26
  super().__init__(f'{status}: {reason}')
27
27
 
28
28
  def __repr__(self) -> str: # pragma: nocover
29
+ """The error's debug string representation."""
29
30
  return f'<{__class__.__module__}.{__class__.__name__} status={self.status} reason={self.reason!r}>'
@@ -113,6 +113,7 @@ class HourlyForecast(BaseForecast):
113
113
  super().__init__(json, unit, locale)
114
114
 
115
115
  def __repr__(self) -> str:
116
+ """The forecast's debug string representation."""
116
117
  return f'<{__class__.__module__}.{__class__.__name__} time={self.time!r} temperature={self.temperature} kind={self.kind!r}>'
117
118
 
118
119
 
@@ -201,12 +202,15 @@ class DailyForecast:
201
202
  ...
202
203
 
203
204
  def __repr__(self) -> str:
205
+ """The forecast's debug string representation."""
204
206
  return f'<{__class__.__module__}.{__class__.__name__} date={self.date!r} temperature={self.temperature}>'
205
207
 
206
208
  def __len__(self) -> int:
209
+ """The amount of hourly forecasts."""
207
210
  return len(self.hourly_forecasts)
208
211
 
209
212
  def __iter__(self) -> Iterator[HourlyForecast]:
213
+ """Iterates through the hourly forecasts."""
210
214
  return iter(self.hourly_forecasts)
211
215
 
212
216
 
@@ -272,10 +276,13 @@ class Forecast(BaseForecast):
272
276
  super().__init__(current, unit, locale)
273
277
 
274
278
  def __repr__(self) -> str:
279
+ """The forecast's debug string representation."""
275
280
  return f'<{__class__.__module__}.{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature}>'
276
281
 
277
282
  def __len__(self) -> int:
283
+ """The amount of daily forecasts."""
278
284
  return len(self.daily_forecasts)
279
285
 
280
286
  def __iter__(self) -> Iterator[DailyForecast]:
287
+ """Iterates through the daily forecasts."""
281
288
  return iter(self.daily_forecasts)
@@ -1,4 +1,4 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  # SPDX-FileCopyrightText: 2021-2026 null8626
3
3
 
4
- VERSION = '2.1.2'
4
+ VERSION = '2.2.0'
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-weather
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: A free and asynchronous weather API wrapper made in Python, for Python.
5
5
  Author: null8626
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Documentation, https://python-weather.readthedocs.io/en/latest/
8
8
  Project-URL: Repository, https://github.com/null8626/python-weather
9
9
  Project-URL: Changelog, https://python-weather.readthedocs.io/en/latest/changelog.html
@@ -33,7 +33,7 @@ Classifier: Programming Language :: Python :: 3.14
33
33
  Requires-Python: >=3.10
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: aiohttp>=3.13.3
36
+ Requires-Dist: aiohttp>=3.13.5
37
37
  Dynamic: license-file
38
38
 
39
39
  # [python-weather][pypi-url] [![pypi][pypi-image]][pypi-url] [![pypi downloads][pypi-downloads-image]][pypi-url] [![codacy-badge][codacy-image]][codacy-url] [![codecov-badge][codecov-image]][codecov-url] [![ko-fi][ko-fi-brief-image]][ko-fi-url]
@@ -0,0 +1 @@
1
+ aiohttp>=3.13.5
@@ -1 +0,0 @@
1
- aiohttp>=3.13.3
File without changes
File without changes
File without changes