python-weather 2.1.1__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 (25) hide show
  1. {python_weather-2.1.1 → python_weather-2.2.0}/LICENSE +1 -1
  2. {python_weather-2.1.1/python_weather.egg-info → python_weather-2.2.0}/PKG-INFO +3 -3
  3. {python_weather-2.1.1 → python_weather-2.2.0}/pyproject.toml +3 -3
  4. python_weather-2.2.0/python_weather/__init__.py +34 -0
  5. python_weather-2.2.0/python_weather/base.py +82 -0
  6. {python_weather-2.1.1 → python_weather-2.2.0}/python_weather/client.py +66 -36
  7. python_weather-2.2.0/python_weather/constants.py +72 -0
  8. {python_weather-2.1.1 → python_weather-2.2.0}/python_weather/enums.py +37 -119
  9. python_weather-2.2.0/python_weather/errors.py +30 -0
  10. {python_weather-2.1.1 → python_weather-2.2.0}/python_weather/forecast.py +9 -23
  11. python_weather-2.2.0/python_weather/version.py +4 -0
  12. {python_weather-2.1.1 → python_weather-2.2.0/python_weather.egg-info}/PKG-INFO +3 -3
  13. python_weather-2.2.0/python_weather.egg-info/requires.txt +1 -0
  14. python_weather-2.1.1/python_weather/__init__.py +0 -55
  15. python_weather-2.1.1/python_weather/base.py +0 -156
  16. python_weather-2.1.1/python_weather/constants.py +0 -69
  17. python_weather-2.1.1/python_weather/errors.py +0 -50
  18. python_weather-2.1.1/python_weather/version.py +0 -1
  19. python_weather-2.1.1/python_weather.egg-info/requires.txt +0 -1
  20. {python_weather-2.1.1 → python_weather-2.2.0}/MANIFEST.in +0 -0
  21. {python_weather-2.1.1 → python_weather-2.2.0}/README.md +0 -0
  22. {python_weather-2.1.1 → python_weather-2.2.0}/python_weather.egg-info/SOURCES.txt +0 -0
  23. {python_weather-2.1.1 → python_weather-2.2.0}/python_weather.egg-info/dependency_links.txt +0 -0
  24. {python_weather-2.1.1 → python_weather-2.2.0}/python_weather.egg-info/top_level.txt +0 -0
  25. {python_weather-2.1.1 → python_weather-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021-2025 null8626
3
+ Copyright (c) 2021-2026 null8626
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-weather
3
- Version: 2.1.1
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.2
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.1"
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.2"]
12
+ dependencies = ["aiohttp>=3.13.5"]
13
13
  classifiers = [
14
14
  "Development Status :: 5 - Production/Stable",
15
15
  "Intended Audience :: Education",
@@ -0,0 +1,34 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
3
+
4
+ from .enums import HeatIndex, Kind, Locale, Phase, UltraViolet, WindDirection
5
+ from .constants import METRIC, IMPERIAL
6
+ from .errors import Error, RequestError
7
+ from .forecast import Forecast
8
+ from .version import VERSION
9
+ from .client import Client
10
+
11
+
12
+ __title__ = 'python-weather'
13
+ __author__ = 'null8626'
14
+ __credits__ = (__author__,)
15
+ __maintainer__ = __author__
16
+ __status__ = 'Production'
17
+ __license__ = 'MIT'
18
+ __copyright__ = 'Copyright (c) 2021-2026 null8626'
19
+ __version__ = VERSION
20
+ __all__ = (
21
+ 'METRIC',
22
+ 'IMPERIAL',
23
+ 'Client',
24
+ 'Error',
25
+ 'Forecast',
26
+ 'RequestError',
27
+ 'HeatIndex',
28
+ 'Kind',
29
+ 'Locale',
30
+ 'Phase',
31
+ 'UltraViolet',
32
+ 'VERSION',
33
+ 'WindDirection',
34
+ )
@@ -0,0 +1,82 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
3
+
4
+ from .enums import WindDirection, Kind, Locale, UltraViolet
5
+ from .constants import _Unit
6
+
7
+
8
+ class BaseForecast:
9
+ """A base weather forecast."""
10
+
11
+ __slots__: tuple[str, ...] = (
12
+ 'cloud_cover',
13
+ 'ultraviolet',
14
+ 'humidity',
15
+ 'wind_direction',
16
+ 'kind',
17
+ 'feels_like',
18
+ 'temperature',
19
+ 'precipitation',
20
+ 'pressure',
21
+ 'visibility',
22
+ 'wind_speed',
23
+ 'description',
24
+ )
25
+
26
+ cloud_cover: int
27
+ """The cloud cover value in percent."""
28
+
29
+ ultraviolet: UltraViolet
30
+ """The ultra-violet index."""
31
+
32
+ humidity: int
33
+ """The humidity value in percent."""
34
+
35
+ wind_direction: WindDirection
36
+ """The wind direction."""
37
+
38
+ kind: Kind
39
+ """The kind of the forecast."""
40
+
41
+ feels_like: int
42
+ """What it felt like in either celcius or fahrenheit."""
43
+
44
+ temperature: int
45
+ """The temperature in either celcius or fahrenheit."""
46
+
47
+ precipitation: float
48
+ """The precipitation in either millimeters or inches."""
49
+
50
+ pressure: float
51
+ """The pressure in either pascal or inches."""
52
+
53
+ visibility: int
54
+ """The visibility distance in either kilometers or miles."""
55
+
56
+ wind_speed: int
57
+ """The wind speeds in either kilometers/hour or miles/hour."""
58
+
59
+ description: str
60
+ """The description regarding the forecast depending on the localization used."""
61
+
62
+ def __init__(self, json: dict, unit: _Unit, locale: Locale):
63
+ description = (
64
+ json['weatherDesc'][0]['value']
65
+ if locale is Locale.ENGLISH
66
+ else json[f'lang_{locale.value}'][0]['value']
67
+ )
68
+
69
+ self.cloud_cover = int(json['cloudcover'])
70
+ self.ultraviolet = UltraViolet._new(int(json['uvIndex']))
71
+ self.humidity = int(json['humidity'])
72
+ self.wind_direction = WindDirection._new(
73
+ json['winddir16Point'], int(json['winddirDegree'])
74
+ )
75
+ self.kind = Kind(int(json['weatherCode']))
76
+ self.feels_like = int(json[f'FeelsLike{unit.temperature}'])
77
+ self.temperature = int(json[f'temp_{unit.temperature}'])
78
+ self.precipitation = float(json[f'precip{unit.precipitation}'])
79
+ self.pressure = float(json[f'pressure{unit.pressure}'])
80
+ self.visibility = int(json[f'visibility{unit.visibility}'])
81
+ self.wind_speed = int(json[f'windspeed{unit.velocity}'])
82
+ self.description = description.strip()
@@ -1,26 +1,5 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
24
3
 
25
4
  from aiohttp import ClientSession, ClientTimeout, ClientResponseError, TCPConnector
26
5
  from urllib.parse import quote_plus
@@ -28,13 +7,12 @@ from asyncio import sleep
28
7
 
29
8
  from .errors import Error, RequestError
30
9
  from .constants import _Unit, METRIC
31
- from .base import CustomizableBase
32
10
  from .forecast import Forecast
33
11
  from .version import VERSION
34
12
  from .enums import Locale
35
13
 
36
14
 
37
- class Client(CustomizableBase):
15
+ class Client:
38
16
  """
39
17
  Interact with the API's endpoints.
40
18
 
@@ -66,11 +44,19 @@ class Client(CustomizableBase):
66
44
  :exception Error: ``unit`` is not :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL` or ``locale`` is not a part of the :class:`.Locale` enum.
67
45
  """
68
46
 
69
- __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
+ )
70
54
 
71
55
  __own_session: bool
72
56
  __session: ClientSession
73
57
  _max_retries: int
58
+ _unit: _Unit
59
+ _locale: Locale
74
60
 
75
61
  def __init__(
76
62
  self,
@@ -80,18 +66,58 @@ class Client(CustomizableBase):
80
66
  session: ClientSession | None = None,
81
67
  max_retries: int | None = None,
82
68
  ):
83
- super().__init__(unit, locale)
84
-
85
69
  self.__own_session = session is None
86
70
  self.__session = session or ClientSession(
87
71
  timeout=ClientTimeout(total=5000.0),
88
72
  connector=TCPConnector(ssl=False),
89
73
  )
90
74
  self._max_retries = max_retries or 3
75
+ self.unit = unit
76
+ self.locale = locale
91
77
 
92
78
  def __repr__(self) -> str:
79
+ """The client's debug string representation."""
93
80
  return f'<{__class__.__module__}.{__class__.__name__} {self.__session!r}>'
94
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
+
95
121
  async def get(
96
122
  self,
97
123
  location: str,
@@ -115,19 +141,20 @@ class Client(CustomizableBase):
115
141
  :param locale: Overrides the locale used.
116
142
  :type locale: :class:`.Locale` | :py:obj:`None`
117
143
 
118
- :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.
119
146
  :exception Error: The client is already closed.
120
147
  :exception RequestError: The client received a non-favorable response from the API.
121
148
 
122
149
  :returns: The requested weather forecast.
123
150
  :rtype: Forecast
124
151
  """
125
-
126
152
  if self.__session.closed:
127
153
  raise Error('Client session is already closed.')
128
-
129
- elif not isinstance(location, str) or not location:
130
- 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.')
131
158
 
132
159
  if not isinstance(unit, _Unit):
133
160
  unit = self._unit
@@ -155,7 +182,9 @@ class Client(CustomizableBase):
155
182
 
156
183
  resp.raise_for_status()
157
184
 
158
- return Forecast(await resp.json(), unit, locale)
185
+ return Forecast(
186
+ await resp.json(content_type='application/text'), unit, locale
187
+ )
159
188
  except ClientResponseError:
160
189
  if attempts == self._max_retries:
161
190
  raise RequestError(status, reason) from None
@@ -165,7 +194,7 @@ class Client(CustomizableBase):
165
194
 
166
195
  async def close(self) -> None:
167
196
  """
168
- Closes the :class:`.Client` object.
197
+ Closes the client.
169
198
 
170
199
  Example:
171
200
 
@@ -173,12 +202,13 @@ class Client(CustomizableBase):
173
202
 
174
203
  await client.close()
175
204
  """
176
-
177
205
  if self.__own_session and not self.__session.closed:
178
206
  await self.__session.close()
179
207
 
180
208
  async def __aenter__(self) -> 'Client':
209
+ """Starts using the client. This method is no-op and just returns itself."""
181
210
  return self
182
211
 
183
212
  async def __aexit__(self, *_, **__) -> None:
213
+ """Closes the client."""
184
214
  await self.close()
@@ -0,0 +1,72 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
3
+
4
+ import re
5
+
6
+
7
+ class _Unit:
8
+ """A supported measurement unit."""
9
+
10
+ __slots__: tuple[str, ...] = (
11
+ 'temperature',
12
+ 'velocity',
13
+ 'pressure',
14
+ 'precipitation',
15
+ 'visibility',
16
+ 'cm_divisor',
17
+ )
18
+
19
+ temperature: str
20
+ velocity: str
21
+ pressure: str
22
+ precipitation: str
23
+ visibility: str
24
+ cm_divisor: float | int
25
+
26
+ def __init__(
27
+ self,
28
+ temperature: str,
29
+ velocity: str,
30
+ pressure: str,
31
+ precipitation: str,
32
+ visibility: str,
33
+ cm_divisor: float | int,
34
+ ):
35
+ self.temperature = temperature
36
+ self.velocity = velocity
37
+ self.pressure = pressure
38
+ self.precipitation = precipitation
39
+ self.visibility = visibility
40
+ self.cm_divisor = cm_divisor
41
+
42
+ def __repr__(self) -> str:
43
+ """The unit's debug string representation."""
44
+ return f'<Unit [{self.temperature}, {self.velocity}]>'
45
+
46
+
47
+ METRIC = _Unit('C', 'Kmph', '', 'MM', '', 1)
48
+ IMPERIAL = _Unit('F', 'Miles', 'Inches', 'Inches', 'Miles', 2.54)
49
+
50
+ LATLON_REGEX = re.compile(r'^Lat (\-?[\d\.]+) and Lon (\-?[\d\.]+)$')
51
+
52
+ KIND_EMOJIS = (
53
+ '☀️',
54
+ '⛅️',
55
+ '☁️',
56
+ '☁️',
57
+ '🌫',
58
+ '🌦',
59
+ '🌧',
60
+ '🌧',
61
+ '⛈',
62
+ '🌨',
63
+ '❄️',
64
+ '🌦',
65
+ '🌧',
66
+ '🌧',
67
+ '🌨',
68
+ '❄️',
69
+ '🌩',
70
+ '⛈',
71
+ )
72
+ WIND_DIRECTION_EMOJIS = '↑', '↖', '←', '↙', '↓', '↘', '→', '↗'
@@ -1,73 +1,70 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
24
3
 
25
4
  from enum import Enum
26
5
 
27
- from .constants import WIND_DIRECTION_EMOJIS
6
+ from .constants import KIND_EMOJIS, WIND_DIRECTION_EMOJIS
28
7
 
29
8
 
30
9
  class BasicEnum(Enum):
10
+ """An ordinary enum."""
11
+
31
12
  __slots__: tuple[str, ...] = ()
32
13
 
33
14
  def __repr__(self) -> str:
15
+ """The enum's debug string representation."""
34
16
  return f'{self.__class__.__name__}.{self.name}'
35
17
 
36
18
  def __str__(self) -> str:
19
+ """The enum's friendly name."""
37
20
  return self.name.replace('_', ' ').title()
38
21
 
22
+ @property
23
+ def _index(self) -> int:
24
+ return next(filter(lambda kind: self is kind[1], enumerate(self.__class__)))[0]
25
+
39
26
 
40
27
  class IndexedEnum(Enum):
28
+ """An enum that carries an index value."""
29
+
41
30
  __slots__: tuple[str, ...] = ('index',)
42
31
 
43
32
  index: int
44
33
  """The index value."""
45
34
 
46
35
  def __lt__(self, other: 'IndexedEnum | float | int') -> bool:
36
+ """Checks if the enum's index value is less than the other."""
47
37
  return float(self.index) < float(other)
48
38
 
49
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."""
50
41
  return float(self.index) <= float(other)
51
42
 
52
43
  def __eq__(self, other: object) -> bool:
44
+ """Checks if the enum's index value is equal to the other."""
53
45
  if other_float := getattr(other, '__float__', None):
54
46
  return float(self.index) == other_float()
55
47
 
56
48
  return False # pragma: nocover
57
49
 
58
50
  def __gt__(self, other: 'IndexedEnum | float | int') -> bool:
51
+ """Checks if the enum's index value is greater than the other."""
59
52
  return float(self.index) > float(other)
60
53
 
61
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."""
62
56
  return float(self.index) >= float(other)
63
57
 
64
58
  def __hash__(self) -> int:
59
+ """The enum's index value."""
65
60
  return self.index
66
61
 
67
62
  def __int__(self) -> int:
63
+ """The enum's index value."""
68
64
  return self.index
69
65
 
70
66
  def __float__(self) -> float:
67
+ """The floating point representation of the enum's index value."""
71
68
  return float(self.index)
72
69
 
73
70
 
@@ -157,7 +154,7 @@ class WindDirection(BasicEnum):
157
154
  NORTH_NORTHWEST = 'NNW'
158
155
 
159
156
  degrees: float
160
- """The wind direction's value in degrees."""
157
+ """The wind direction's angle in degrees."""
161
158
 
162
159
  @staticmethod
163
160
  def _new(value: str, degrees: float) -> 'WindDirection':
@@ -167,56 +164,27 @@ class WindDirection(BasicEnum):
167
164
  return enum
168
165
 
169
166
  def __contains__(self, other: 'WindDirection | float | int') -> bool:
170
- other = float(other)
171
-
172
- if self is self.NORTH:
173
- return other > 348.75 or other <= 11.25
174
- elif self is self.NORTH_NORTHEAST:
175
- return 11.25 < other <= 33.75
176
- elif self is self.NORTHEAST:
177
- return 33.75 < other <= 56.25
178
- elif self is self.EAST_NORTHEAST:
179
- return 56.25 < other <= 78.75
180
- elif self is self.EAST:
181
- return 78.75 < other <= 101.25
182
- elif self is self.EAST_SOUTHEAST:
183
- return 101.25 < other <= 123.75
184
- elif self is self.SOUTHEAST:
185
- return 123.75 < other <= 146.25
186
- elif self is self.SOUTH_SOUTHEAST:
187
- return 146.25 < other <= 168.75
188
- elif self is self.SOUTH:
189
- return 168.75 < other <= 191.25
190
- elif self is self.SOUTH_SOUTHWEST:
191
- return 191.25 < other <= 213.75
192
- elif self is self.SOUTHWEST:
193
- return 213.75 < other <= 236.25
194
- elif self is self.WEST_SOUTHWEST:
195
- return 236.25 < other <= 258.75
196
- elif self is self.WEST:
197
- return 258.75 < other <= 281.25
198
- elif self is self.WEST_NORTHWEST:
199
- return 281.25 < other <= 303.75
200
- elif self is self.NORTHWEST:
201
- return 303.75 < other <= 326.25
202
- else:
203
- return 326.25 < other <= 348.75
167
+ """Checks if the other's angle is within the enum's wind direction."""
168
+ members = list(self.__class__)
169
+
170
+ return self is members[int(((float(other) % 360) + 11.25) // 22.5) % len(members)]
204
171
 
205
172
  def __int__(self) -> int:
173
+ """The integer representation of the wind direction's angle."""
206
174
  return int(self.degrees)
207
175
 
208
176
  def __float__(self) -> float:
177
+ """The wind direction's angle."""
209
178
  return self.degrees
210
179
 
211
180
  @property
212
181
  def emoji(self) -> str:
213
- """Emoji representation."""
214
-
215
- return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360) / 45.0)]
182
+ """The wind direction's emoji representation."""
183
+ return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360) // 45)]
216
184
 
217
185
 
218
186
  class Locale(Enum):
219
- """Supported locales/languages."""
187
+ """A supported locale."""
220
188
 
221
189
  __slots__: tuple[str, ...] = ()
222
190
 
@@ -294,9 +262,11 @@ class Locale(Enum):
294
262
  ZULU = 'zu'
295
263
 
296
264
  def __repr__(self) -> str:
265
+ """The locale's debug string representation."""
297
266
  return f'{__class__.__name__}.{self.name}'
298
267
 
299
268
  def __str__(self) -> str:
269
+ """The locale's friendly name."""
300
270
  arr = self.name.title().split('_')
301
271
 
302
272
  return f'{" ".join(arr[:-1])} ({arr[-1]})' if len(arr) != 1 else arr[0]
@@ -355,44 +325,8 @@ class Kind(BasicEnum):
355
325
 
356
326
  @property
357
327
  def emoji(self) -> str:
358
- """Emoji representation."""
359
-
360
- if self is self.CLOUDY:
361
- return '☁️'
362
- elif self is self.FOG:
363
- return '🌫'
364
- elif self is self.HEAVY_RAIN:
365
- return '🌧'
366
- elif self is self.HEAVY_SHOWERS:
367
- return '🌧'
368
- elif self is self.HEAVY_SNOW:
369
- return '❄️'
370
- elif self is self.HEAVY_SNOW_SHOWERS:
371
- return '❄️'
372
- elif self is self.LIGHT_RAIN:
373
- return '🌦'
374
- elif self is self.LIGHT_SHOWERS:
375
- return '🌦'
376
- elif self is self.LIGHT_SLEET:
377
- return '🌧'
378
- elif self is self.LIGHT_SLEET_SHOWERS:
379
- return '🌧'
380
- elif self is self.LIGHT_SNOW:
381
- return '🌨'
382
- elif self is self.LIGHT_SNOW_SHOWERS:
383
- return '🌨'
384
- elif self is self.PARTLY_CLOUDY:
385
- return '⛅️'
386
- elif self is self.SUNNY:
387
- return '☀️'
388
- elif self is self.THUNDERY_HEAVY_RAIN:
389
- return '🌩'
390
- elif self is self.THUNDERY_SHOWERS:
391
- return '⛈'
392
- elif self is self.THUNDERY_SNOW_SHOWERS:
393
- return '⛈'
394
- else:
395
- return '☁️'
328
+ """The weather forecast kind's emoji representation."""
329
+ return KIND_EMOJIS[self._index]
396
330
 
397
331
 
398
332
  class Phase(BasicEnum):
@@ -411,21 +345,5 @@ class Phase(BasicEnum):
411
345
 
412
346
  @property
413
347
  def emoji(self) -> str:
414
- """Emoji representation."""
415
-
416
- if self is self.NEW_MOON:
417
- return '🌑'
418
- elif self is self.WAXING_CRESCENT:
419
- return '🌒'
420
- elif self is self.FIRST_QUARTER:
421
- return '🌓'
422
- elif self is self.WAXING_GIBBOUS:
423
- return '🌔'
424
- elif self is self.FULL_MOON:
425
- return '🌕'
426
- elif self is self.WANING_GIBBOUS:
427
- return '🌖'
428
- elif self is self.LAST_QUARTER:
429
- return '🌗'
430
- else:
431
- return '🌘'
348
+ """The moon phase's emoji representation."""
349
+ return chr(0x1F311 + self._index)
@@ -0,0 +1,30 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
3
+
4
+
5
+ class Error(Exception):
6
+ """The base error class. Extends :py:class:`Exception`."""
7
+
8
+ __slots__: tuple[str, ...] = ()
9
+
10
+
11
+ class RequestError(Error):
12
+ """Thrown upon HTTP request failure. Extends :class:`.Error`."""
13
+
14
+ __slots__: tuple[str, ...] = 'status', 'reason'
15
+
16
+ status: int | None
17
+ """The status code."""
18
+
19
+ reason: str | None
20
+ """The reason for this status code."""
21
+
22
+ def __init__(self, status: int | None, reason: str | None):
23
+ self.status = status
24
+ self.reason = reason
25
+
26
+ super().__init__(f'{status}: {reason}')
27
+
28
+ def __repr__(self) -> str: # pragma: nocover
29
+ """The error's debug string representation."""
30
+ return f'<{__class__.__module__}.{__class__.__name__} status={self.status} reason={self.reason!r}>'
@@ -1,26 +1,5 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
24
3
 
25
4
  from datetime import datetime, date, time
26
5
  from collections.abc import Iterator
@@ -134,6 +113,7 @@ class HourlyForecast(BaseForecast):
134
113
  super().__init__(json, unit, locale)
135
114
 
136
115
  def __repr__(self) -> str:
116
+ """The forecast's debug string representation."""
137
117
  return f'<{__class__.__module__}.{__class__.__name__} time={self.time!r} temperature={self.temperature} kind={self.kind!r}>'
138
118
 
139
119
 
@@ -222,12 +202,15 @@ class DailyForecast:
222
202
  ...
223
203
 
224
204
  def __repr__(self) -> str:
205
+ """The forecast's debug string representation."""
225
206
  return f'<{__class__.__module__}.{__class__.__name__} date={self.date!r} temperature={self.temperature}>'
226
207
 
227
208
  def __len__(self) -> int:
209
+ """The amount of hourly forecasts."""
228
210
  return len(self.hourly_forecasts)
229
211
 
230
212
  def __iter__(self) -> Iterator[HourlyForecast]:
213
+ """Iterates through the hourly forecasts."""
231
214
  return iter(self.hourly_forecasts)
232
215
 
233
216
 
@@ -293,10 +276,13 @@ class Forecast(BaseForecast):
293
276
  super().__init__(current, unit, locale)
294
277
 
295
278
  def __repr__(self) -> str:
279
+ """The forecast's debug string representation."""
296
280
  return f'<{__class__.__module__}.{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature}>'
297
281
 
298
282
  def __len__(self) -> int:
283
+ """The amount of daily forecasts."""
299
284
  return len(self.daily_forecasts)
300
285
 
301
286
  def __iter__(self) -> Iterator[DailyForecast]:
287
+ """Iterates through the daily forecasts."""
302
288
  return iter(self.daily_forecasts)
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # SPDX-FileCopyrightText: 2021-2026 null8626
3
+
4
+ VERSION = '2.2.0'
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-weather
3
- Version: 2.1.1
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.2
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,55 +0,0 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
24
-
25
- from .enums import HeatIndex, Kind, Locale, Phase, UltraViolet, WindDirection
26
- from .constants import METRIC, IMPERIAL
27
- from .errors import Error, RequestError
28
- from .forecast import Forecast
29
- from .version import VERSION
30
- from .client import Client
31
-
32
-
33
- __title__ = 'python-weather'
34
- __author__ = 'null8626'
35
- __credits__ = (__author__,)
36
- __maintainer__ = __author__
37
- __status__ = 'Production'
38
- __license__ = 'MIT'
39
- __copyright__ = 'Copyright (c) 2021-2025 null8626'
40
- __version__ = VERSION
41
- __all__ = (
42
- 'METRIC',
43
- 'IMPERIAL',
44
- 'Client',
45
- 'Error',
46
- 'Forecast',
47
- 'RequestError',
48
- 'HeatIndex',
49
- 'Kind',
50
- 'Locale',
51
- 'Phase',
52
- 'UltraViolet',
53
- 'VERSION',
54
- 'WindDirection',
55
- )
@@ -1,156 +0,0 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
24
-
25
- from .enums import WindDirection, Kind, Locale, UltraViolet
26
- from .constants import _Unit
27
- from .errors import Error
28
-
29
-
30
- class CustomizableBase:
31
- __slots__: tuple[str, ...] = '_unit', '_locale'
32
-
33
- _unit: _Unit
34
- _locale: Locale
35
-
36
- def __init__(self, unit: _Unit, locale: Locale):
37
- self.unit = unit
38
- self.locale = locale
39
-
40
- @property
41
- def unit(self) -> _Unit:
42
- """The measuring unit used."""
43
-
44
- return self._unit
45
-
46
- @unit.setter
47
- def unit(self, to: _Unit) -> None:
48
- """
49
- Sets the default measuring unit used.
50
-
51
- :param to: The new default measuring unit to be used.
52
-
53
- :exception Error: ``to`` is not either :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL`.
54
- """
55
-
56
- if not isinstance(to, _Unit):
57
- raise Error('Invalid measuring unit specified!')
58
-
59
- self._unit = to
60
-
61
- @property
62
- def locale(self) -> Locale:
63
- """The localization used."""
64
-
65
- return self._locale
66
-
67
- @locale.setter
68
- def locale(self, to: Locale) -> None:
69
- """
70
- Sets the default localization used.
71
-
72
- :param to: The new :class:`.Locale` to be used.
73
- :type to: :class:`.Locale`
74
-
75
- :exception Error: ``to`` is not a part of the :class:`.Locale` enum.
76
- """
77
-
78
- if not isinstance(to, Locale):
79
- raise Error(f'Expected {to!r} to be a Locale enum')
80
-
81
- self._locale = to
82
-
83
-
84
- class BaseForecast:
85
- __slots__: tuple[str, ...] = (
86
- 'cloud_cover',
87
- 'ultraviolet',
88
- 'humidity',
89
- 'wind_direction',
90
- 'kind',
91
- 'feels_like',
92
- 'temperature',
93
- 'precipitation',
94
- 'pressure',
95
- 'visibility',
96
- 'wind_speed',
97
- 'description',
98
- )
99
-
100
- cloud_cover: int
101
- """The cloud cover value in percent."""
102
-
103
- ultraviolet: UltraViolet
104
- """The ultra-violet index."""
105
-
106
- humidity: int
107
- """The humidity value in percent."""
108
-
109
- wind_direction: WindDirection
110
- """The wind direction."""
111
-
112
- kind: Kind
113
- """The kind of the forecast."""
114
-
115
- feels_like: int
116
- """What it felt like in either celcius or fahrenheit."""
117
-
118
- temperature: int
119
- """The temperature in either celcius or fahrenheit."""
120
-
121
- precipitation: float
122
- """The precipitation in either millimeters or inches."""
123
-
124
- pressure: float
125
- """The pressure in either pascal or inches."""
126
-
127
- visibility: int
128
- """The visibility distance in either kilometers or miles."""
129
-
130
- wind_speed: int
131
- """The wind speeds in either kilometers/hour or miles/hour."""
132
-
133
- description: str
134
- """The description regarding the forecast depending on the localization used."""
135
-
136
- def __init__(self, json: dict, unit: _Unit, locale: Locale):
137
- description = (
138
- json['weatherDesc'][0]['value']
139
- if locale is Locale.ENGLISH
140
- else json[f'lang_{locale.value}'][0]['value']
141
- )
142
-
143
- self.cloud_cover = int(json['cloudcover'])
144
- self.ultraviolet = UltraViolet._new(int(json['uvIndex']))
145
- self.humidity = int(json['humidity'])
146
- self.wind_direction = WindDirection._new(
147
- json['winddir16Point'], int(json['winddirDegree'])
148
- )
149
- self.kind = Kind(int(json['weatherCode']))
150
- self.feels_like = int(json[f'FeelsLike{unit.temperature}'])
151
- self.temperature = int(json[f'temp_{unit.temperature}'])
152
- self.precipitation = float(json[f'precip{unit.precipitation}'])
153
- self.pressure = float(json[f'pressure{unit.pressure}'])
154
- self.visibility = int(json[f'visibility{unit.visibility}'])
155
- self.wind_speed = int(json[f'windspeed{unit.velocity}'])
156
- self.description = description.strip()
@@ -1,69 +0,0 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
24
-
25
- import re
26
-
27
-
28
- class _Unit:
29
- __slots__: tuple[str, ...] = (
30
- 'temperature',
31
- 'velocity',
32
- 'pressure',
33
- 'precipitation',
34
- 'visibility',
35
- 'cm_divisor',
36
- )
37
-
38
- temperature: str
39
- velocity: str
40
- pressure: str
41
- precipitation: str
42
- visibility: str
43
- cm_divisor: float | int
44
-
45
- def __init__(
46
- self,
47
- temperature: str,
48
- velocity: str,
49
- pressure: str,
50
- precipitation: str,
51
- visibility: str,
52
- cm_divisor: float | int,
53
- ):
54
- self.temperature = temperature
55
- self.velocity = velocity
56
- self.pressure = pressure
57
- self.precipitation = precipitation
58
- self.visibility = visibility
59
- self.cm_divisor = cm_divisor
60
-
61
- def __repr__(self) -> str:
62
- return f'<Unit [{self.temperature}, {self.velocity}]>'
63
-
64
-
65
- METRIC = _Unit('C', 'Kmph', '', 'MM', '', 1)
66
- IMPERIAL = _Unit('F', 'Miles', 'Inches', 'Inches', 'Miles', 2.54)
67
-
68
- WIND_DIRECTION_EMOJIS = '↓', '↙', '←', '↖', '↑', '↗', '→', '↘'
69
- LATLON_REGEX = re.compile(r'^Lat (\-?[\d\.]+) and Lon (\-?[\d\.]+)$')
@@ -1,50 +0,0 @@
1
- """
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2021-2025 null8626
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- """
24
-
25
-
26
- class Error(Exception):
27
- """The base error class. Extends :py:class:`Exception`."""
28
-
29
- __slots__: tuple[str, ...] = ()
30
-
31
-
32
- class RequestError(Error):
33
- """Thrown upon HTTP request failure. Extends :class:`.Error`."""
34
-
35
- __slots__: tuple[str, ...] = 'status', 'reason'
36
-
37
- status: int | None
38
- """The status code."""
39
-
40
- reason: str | None
41
- """The reason for this status code."""
42
-
43
- def __init__(self, status: int | None, reason: str | None):
44
- self.status = status
45
- self.reason = reason
46
-
47
- super().__init__(f'{status}: {reason}')
48
-
49
- def __repr__(self) -> str: # pragma: nocover
50
- return f'<{__class__.__module__}.{__class__.__name__} status={self.status} reason={self.reason!r}>'
@@ -1 +0,0 @@
1
- VERSION = '3.0.0'
@@ -1 +0,0 @@
1
- aiohttp>=3.13.2
File without changes
File without changes