python-weather 2.0.6__tar.gz → 2.1.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.0.6 → python_weather-2.1.0}/LICENSE +1 -1
  2. {python_weather-2.0.6/python_weather.egg-info → python_weather-2.1.0}/PKG-INFO +24 -18
  3. {python_weather-2.0.6 → python_weather-2.1.0}/README.md +19 -13
  4. {python_weather-2.0.6 → python_weather-2.1.0}/pyproject.toml +3 -4
  5. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/__init__.py +6 -3
  6. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/base.py +19 -19
  7. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/client.py +38 -31
  8. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/constants.py +4 -4
  9. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/enums.py +41 -32
  10. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/errors.py +13 -10
  11. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather/forecast.py +26 -22
  12. python_weather-2.1.0/python_weather/version.py +1 -0
  13. {python_weather-2.0.6 → python_weather-2.1.0/python_weather.egg-info}/PKG-INFO +24 -18
  14. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather.egg-info/SOURCES.txt +1 -0
  15. python_weather-2.1.0/python_weather.egg-info/requires.txt +1 -0
  16. python_weather-2.0.6/python_weather.egg-info/requires.txt +0 -1
  17. {python_weather-2.0.6 → python_weather-2.1.0}/MANIFEST.in +0 -0
  18. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather.egg-info/dependency_links.txt +0 -0
  19. {python_weather-2.0.6 → python_weather-2.1.0}/python_weather.egg-info/top_level.txt +0 -0
  20. {python_weather-2.0.6 → python_weather-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021-2024 null8626
3
+ Copyright (c) 2021-2025 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,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-weather
3
- Version: 2.0.6
3
+ Version: 2.1.0
4
4
  Summary: A free and asynchronous weather API wrapper made in Python, for Python.
5
5
  Author: null8626
6
6
  License: MIT
@@ -26,16 +26,16 @@ Classifier: License :: OSI Approved :: MIT License
26
26
  Classifier: Programming Language :: Python
27
27
  Classifier: Programming Language :: Python :: 3
28
28
  Classifier: Programming Language :: Python :: 3 :: Only
29
- Classifier: Programming Language :: Python :: 3.8
30
29
  Classifier: Programming Language :: Python :: 3.9
31
30
  Classifier: Programming Language :: Python :: 3.10
32
31
  Classifier: Programming Language :: Python :: 3.11
33
32
  Classifier: Programming Language :: Python :: 3.12
34
33
  Classifier: Programming Language :: Python :: 3.13
35
- Requires-Python: >=3.8
34
+ Requires-Python: >=3.9
36
35
  Description-Content-Type: text/markdown
37
36
  License-File: LICENSE
38
- Requires-Dist: aiohttp>=3.10.10
37
+ Requires-Dist: aiohttp>=3.12.4
38
+ Dynamic: license-file
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
 
@@ -50,10 +50,12 @@ Requires-Dist: aiohttp>=3.10.10
50
50
 
51
51
  A free and asynchronous weather Python API wrapper made in Python, for Python.
52
52
 
53
- ## Installation
53
+ ## Getting started
54
+
55
+ Run the following command in your terminal:
54
56
 
55
57
  ```console
56
- pip install python-weather
58
+ $ pip install python-weather
57
59
  ```
58
60
 
59
61
  ## Example
@@ -61,36 +63,40 @@ pip install python-weather
61
63
  For more information, please read the [documentation](https://python-weather.readthedocs.io/en/latest/).
62
64
 
63
65
  ```py
64
- # import the module
66
+ # Import the module.
65
67
  import python_weather
66
68
 
67
69
  import asyncio
68
70
  import os
69
71
 
70
- async def getweather() -> None:
71
- # declare the client. the measuring unit used defaults to the metric system (celcius, km/h, etc.)
72
+
73
+ async def main() -> None:
74
+
75
+ # Declare the client. The measuring unit used defaults to the metric system (celcius, km/h, etc.)
72
76
  async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
73
- # fetch a weather forecast from a city
77
+
78
+ # Fetch a weather forecast from a city.
74
79
  weather = await client.get('New York')
75
80
 
76
- # returns the current day's forecast temperature (int)
81
+ # Fetch the temperature for today.
77
82
  print(weather.temperature)
78
83
 
79
- # get the weather forecast for a few days
84
+ # Fetch weather forecast for upcoming days.
80
85
  for daily in weather:
81
86
  print(daily)
82
-
83
- # hourly forecasts
87
+
88
+ # Each daily forecast has their own hourly forecasts.
84
89
  for hourly in daily:
85
90
  print(f' --> {hourly!r}')
86
91
 
87
92
  if __name__ == '__main__':
88
- # see https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
89
- # for more details
93
+
94
+ # See https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
95
+ # for more details.
90
96
  if os.name == 'nt':
91
97
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
92
98
 
93
- asyncio.run(getweather())
99
+ asyncio.run(main())
94
100
  ```
95
101
 
96
102
  ## Data source
@@ -11,10 +11,12 @@
11
11
 
12
12
  A free and asynchronous weather Python API wrapper made in Python, for Python.
13
13
 
14
- ## Installation
14
+ ## Getting started
15
+
16
+ Run the following command in your terminal:
15
17
 
16
18
  ```console
17
- pip install python-weather
19
+ $ pip install python-weather
18
20
  ```
19
21
 
20
22
  ## Example
@@ -22,36 +24,40 @@ pip install python-weather
22
24
  For more information, please read the [documentation](https://python-weather.readthedocs.io/en/latest/).
23
25
 
24
26
  ```py
25
- # import the module
27
+ # Import the module.
26
28
  import python_weather
27
29
 
28
30
  import asyncio
29
31
  import os
30
32
 
31
- async def getweather() -> None:
32
- # declare the client. the measuring unit used defaults to the metric system (celcius, km/h, etc.)
33
+
34
+ async def main() -> None:
35
+
36
+ # Declare the client. The measuring unit used defaults to the metric system (celcius, km/h, etc.)
33
37
  async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
34
- # fetch a weather forecast from a city
38
+
39
+ # Fetch a weather forecast from a city.
35
40
  weather = await client.get('New York')
36
41
 
37
- # returns the current day's forecast temperature (int)
42
+ # Fetch the temperature for today.
38
43
  print(weather.temperature)
39
44
 
40
- # get the weather forecast for a few days
45
+ # Fetch weather forecast for upcoming days.
41
46
  for daily in weather:
42
47
  print(daily)
43
-
44
- # hourly forecasts
48
+
49
+ # Each daily forecast has their own hourly forecasts.
45
50
  for hourly in daily:
46
51
  print(f' --> {hourly!r}')
47
52
 
48
53
  if __name__ == '__main__':
49
- # see https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
50
- # for more details
54
+
55
+ # See https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
56
+ # for more details.
51
57
  if os.name == 'nt':
52
58
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
53
59
 
54
- asyncio.run(getweather())
60
+ asyncio.run(main())
55
61
  ```
56
62
 
57
63
  ## Data source
@@ -3,13 +3,13 @@ requires = ["setuptools"]
3
3
 
4
4
  [project]
5
5
  name = "python-weather"
6
- version = "2.0.6"
6
+ version = "2.1.0"
7
7
  description = "A free and asynchronous weather API wrapper made in Python, for Python."
8
8
  readme = "README.md"
9
9
  license = { text = "MIT" }
10
10
  authors = [{ name = "null8626" }]
11
11
  keywords = ["weather", "forecast", "weather-api", "weather-forecast"]
12
- dependencies = ["aiohttp>=3.10.10"]
12
+ dependencies = ["aiohttp>=3.12.4"]
13
13
  classifiers = [
14
14
  "Development Status :: 5 - Production/Stable",
15
15
  "Intended Audience :: Education",
@@ -27,14 +27,13 @@ classifiers = [
27
27
  "Programming Language :: Python",
28
28
  "Programming Language :: Python :: 3",
29
29
  "Programming Language :: Python :: 3 :: Only",
30
- "Programming Language :: Python :: 3.8",
31
30
  "Programming Language :: Python :: 3.9",
32
31
  "Programming Language :: Python :: 3.10",
33
32
  "Programming Language :: Python :: 3.11",
34
33
  "Programming Language :: Python :: 3.12",
35
34
  "Programming Language :: Python :: 3.13"
36
35
  ]
37
- requires-python = ">=3.8"
36
+ requires-python = ">=3.9"
38
37
 
39
38
  [project.urls]
40
39
  Documentation = "https://python-weather.readthedocs.io/en/latest/"
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -25,13 +25,15 @@ SOFTWARE.
25
25
  from .enums import HeatIndex, Kind, Locale, Phase, UltraViolet, WindDirection
26
26
  from .constants import METRIC, IMPERIAL
27
27
  from .errors import Error, RequestError
28
+ from .version import VERSION
28
29
  from .client import Client
29
30
 
31
+
30
32
  __title__ = 'python-weather'
31
33
  __author__ = 'null8626'
32
34
  __license__ = 'MIT'
33
- __copyright__ = 'Copyright (c) 2021-2024 null8626'
34
- __version__ = '2.0.6'
35
+ __copyright__ = 'Copyright (c) 2021-2025 null8626'
36
+ __version__ = VERSION
35
37
  __all__ = (
36
38
  'METRIC',
37
39
  'IMPERIAL',
@@ -43,5 +45,6 @@ __all__ = (
43
45
  'Locale',
44
46
  'Phase',
45
47
  'UltraViolet',
48
+ 'VERSION',
46
49
  'WindDirection',
47
50
  )
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,15 +22,13 @@ 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 typing import Tuple
26
-
27
- from .errors import Error
28
25
  from .enums import WindDirection, Kind, Locale, UltraViolet
29
26
  from .constants import _Unit
27
+ from .errors import Error
30
28
 
31
29
 
32
30
  class CustomizableBase:
33
- __slots__: Tuple[str, ...] = ('__unit', '__locale')
31
+ __slots__: tuple[str, ...] = ('__unit', '__locale')
34
32
 
35
33
  def __init__(self, unit: _Unit, locale: Locale):
36
34
  self.unit = unit
@@ -38,17 +36,18 @@ class CustomizableBase:
38
36
 
39
37
  @property
40
38
  def unit(self) -> _Unit:
41
- """The measuring unit used to display information in this object."""
39
+ """The measuring unit used."""
42
40
 
43
41
  return self.__unit
44
42
 
45
43
  @unit.setter
46
44
  def unit(self, to: _Unit) -> None:
47
45
  """
48
- Sets the default measuring unit used to display information in this object.
46
+ Sets the default measuring unit used.
49
47
 
50
- :param to: The new default measuring unit to be used to display information in this object. Must be either ``METRIC`` or ``IMPERIAL``.
51
- :exception Error: If the ``to`` argument is not either ``METRIC`` or ``IMPERIAL``.
48
+ :param to: The new default measuring unit to be used.
49
+
50
+ :exception Error: ``to`` is not either :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL`.
52
51
  """
53
52
 
54
53
  if not isinstance(to, _Unit):
@@ -58,18 +57,19 @@ class CustomizableBase:
58
57
 
59
58
  @property
60
59
  def locale(self) -> Locale:
61
- """The localization used to display information in this object."""
60
+ """The localization used."""
62
61
 
63
62
  return self.__locale
64
63
 
65
64
  @locale.setter
66
65
  def locale(self, to: Locale) -> None:
67
66
  """
68
- Sets the default localization used to display information in this object.
67
+ Sets the default localization used.
68
+
69
+ :param to: The new :class:`.Locale` to be used.
70
+ :type to: :class:`.Locale`
69
71
 
70
- :param to: The new :class:`~python_weather.enums.Locale` to be used to display information in this object.
71
- :type to: Locale
72
- :exception Error: If the ``to`` argument is not a part of the :class:`~python_weather.enums.Locale` enum.
72
+ :exception Error: ``to`` is not a part of the :class:`.Locale` enum.
73
73
  """
74
74
 
75
75
  if not isinstance(to, Locale):
@@ -79,7 +79,7 @@ class CustomizableBase:
79
79
 
80
80
 
81
81
  class BaseForecast:
82
- __slots__: Tuple[str, ...] = (
82
+ __slots__: tuple[str, ...] = (
83
83
  'ultraviolet',
84
84
  'humidity',
85
85
  'wind_direction',
@@ -94,7 +94,7 @@ class BaseForecast:
94
94
  )
95
95
 
96
96
  ultraviolet: UltraViolet
97
- """The ultra-violet (UV) index."""
97
+ """The ultra-violet index."""
98
98
 
99
99
  humidity: int
100
100
  """The humidity value in percent."""
@@ -106,10 +106,10 @@ class BaseForecast:
106
106
  """The kind of the forecast."""
107
107
 
108
108
  feels_like: int
109
- """What it felt like, in celcius or fahrenheit."""
109
+ """What it felt like in either celcius or fahrenheit."""
110
110
 
111
111
  temperature: int
112
- """The temperature in either celcius or Fahrenheit."""
112
+ """The temperature in either celcius or fahrenheit."""
113
113
 
114
114
  precipitation: float
115
115
  """The precipitation in either millimeters or inches."""
@@ -124,7 +124,7 @@ class BaseForecast:
124
124
  """The wind speeds in either kilometers/hour or miles/hour."""
125
125
 
126
126
  description: str
127
- """The description regarding the forecast. This can be localized in different languages depending on the localization used."""
127
+ """The description regarding the forecast depending on the localization used."""
128
128
 
129
129
  def __init__(self, json: dict, unit: _Unit, locale: Locale):
130
130
  description = (
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,35 +22,36 @@ 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 aiohttp import ClientSession, ClientTimeout, TCPConnector
26
- from typing import Optional, Tuple, Self
25
+ from aiohttp import ClientSession, ClientTimeout, ClientResponseError, TCPConnector
27
26
  from urllib.parse import quote_plus
27
+ from typing import Optional
28
28
  from asyncio import sleep
29
29
 
30
30
  from .errors import Error, RequestError
31
31
  from .constants import _Unit, METRIC
32
32
  from .base import CustomizableBase
33
33
  from .forecast import Forecast
34
+ from .version import VERSION
34
35
  from .enums import Locale
35
36
 
36
37
 
37
38
  class Client(CustomizableBase):
38
39
  """
39
- The class that lets you interact with the API.
40
+ Interact with the API's endpoints.
40
41
 
41
- :param unit: Whether to use the metric or imperial/customary system (``IMPERIAL``). Defaults to ``METRIC``.
42
- :type unit: _Unit
43
- :param locale: Whether to use a different locale/language as the description for the returned forecast. Defaults to ``Locale.ENGLISH``.
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).
42
+ :param unit: Whether to use the metric or imperial/customary system (:data:`~.constants.IMPERIAL`). Defaults to :data:`~.constants.METRIC`.
43
+ :type unit: ``_Unit``
44
+ :param locale: Whether to use a different locale/language as the description for the returned forecast. Defaults to :attr:`.Locale.ENGLISH`.
45
+ :type locale: :class:`.Locale`
46
+ :param session: Whether to use an existing :class:`~aiohttp.ClientSession` for requesting or not. Defaults to :py:obj:`None` (creates a new one instead).
46
47
  :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
+ :param max_retries: Maximum amount of retries upon request failure before raising a :class:`.RequestError`. Use ``-1`` to disable (NOT recommended). Defaults to 3 retries.
48
49
  :type max_retries: Optional[:class:`int`]
49
50
 
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.
51
+ :exception Error: ``unit`` is not :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL` or ``locale`` is not a part of the :class:`.Locale` enum.
51
52
  """
52
53
 
53
- __slots__: Tuple[str, ...] = ('__own_session', '__session', '__max_retries')
54
+ __slots__: tuple[str, ...] = ('__own_session', '__session', '__max_retries')
54
55
 
55
56
  def __init__(
56
57
  self,
@@ -82,24 +83,26 @@ class Client(CustomizableBase):
82
83
  """
83
84
  Fetches a weather forecast for a specific location.
84
85
 
85
- :param location: The requested location name for said weather forecast.
86
- :type location: str
87
- :param unit: Overrides the unit used by this object. Defaults to the one used by this object.
86
+ :param location: The requested location.
87
+ :type location: :py:class:`str`
88
+ :param unit: Overrides the unit used.
88
89
  :type unit: Optional[``_Unit``]
89
- :param locale: Overrides the locale used by this object. Defaults to the one used by this object.
90
- :type locale: Optional[:class:`~python_weather.enums.Locale`]
90
+ :param locale: Overrides the locale used.
91
+ :type locale: Optional[:class:`.Locale`]
91
92
 
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.
93
+ :exception TypeError: ``location`` is not a :py:class:`str` or is empty.
94
+ :exception Error: The client is already closed.
95
+ :exception RequestError: The client received a non-favorable response from the API.
94
96
 
95
97
  :returns: The requested weather forecast.
96
98
  :rtype: Forecast
97
99
  """
98
100
 
99
- if (not isinstance(location, str)) or (not location):
100
- raise Error(f'Expected a proper location str, got {location!r}')
101
- elif self.__session.closed:
102
- raise Error('Client is already closed')
101
+ if self.__session.closed:
102
+ raise Error('Client session is already closed.')
103
+
104
+ elif not isinstance(location, str) or not location:
105
+ raise TypeError(f'Expected a proper location str, got {location!r}')
103
106
 
104
107
  if not isinstance(unit, _Unit):
105
108
  unit = self._CustomizableBase__unit
@@ -108,32 +111,36 @@ class Client(CustomizableBase):
108
111
  locale = self._CustomizableBase__locale
109
112
 
110
113
  subdomain = f'{locale.value}.' if locale != Locale.ENGLISH else ''
111
- delay = 0.5
112
114
  attempts = 0
115
+ status = None
113
116
 
114
117
  while True:
115
118
  try:
116
119
  async with self.__session.get(
117
- f'https://{subdomain}wttr.in/{quote_plus(location)}?format=j1'
120
+ f'https://{subdomain}wttr.in/{quote_plus(location)}?format=j1',
121
+ headers={
122
+ 'Content-Type': 'application/json',
123
+ 'User-Agent': f'python_weather (https://github.com/null8626/python-weather {VERSION}) Python/',
124
+ },
118
125
  ) as resp:
126
+ status = resp.status
119
127
  resp.raise_for_status()
120
128
 
121
129
  return Forecast(await resp.json(), unit, locale)
122
- except Exception as err:
130
+ except ClientResponseError:
123
131
  if attempts == self.__max_retries:
124
- raise RequestError(err)
132
+ raise RequestError(status) from None
125
133
 
126
- await sleep(delay)
134
+ await sleep(0.5 * (2**attempts))
127
135
  attempts += 1
128
- delay *= 2
129
136
 
130
137
  async def close(self) -> None:
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."""
138
+ """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."""
132
139
 
133
140
  if self.__own_session and not self.__session.closed:
134
141
  await self.__session.close()
135
142
 
136
- async def __aenter__(self) -> Self:
143
+ async def __aenter__(self) -> 'Client':
137
144
  return self
138
145
 
139
146
  async def __aexit__(self, *_, **__) -> None:
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,12 +22,12 @@ 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 typing import Union, Tuple
25
+ from typing import Union
26
26
  import re
27
27
 
28
28
 
29
29
  class _Unit:
30
- __slots__: Tuple[str, ...] = (
30
+ __slots__: tuple[str, ...] = (
31
31
  'temperature',
32
32
  'velocity',
33
33
  'pressure',
@@ -43,7 +43,7 @@ class _Unit:
43
43
  pressure: str,
44
44
  precipitation: str,
45
45
  visibility: str,
46
- cm_divisor: Union[int, float],
46
+ cm_divisor: Union[float, int],
47
47
  ):
48
48
  self.temperature = temperature
49
49
  self.velocity = velocity
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,14 +22,14 @@ 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 typing import Union
25
26
  from enum import Enum
26
- from typing import Union, Tuple, Self
27
27
 
28
28
  from .constants import WIND_DIRECTION_EMOJIS
29
29
 
30
30
 
31
31
  class BasicEnum(Enum):
32
- __slots__: Tuple[str, ...] = ()
32
+ __slots__: tuple[str, ...] = ()
33
33
 
34
34
  def __repr__(self) -> str:
35
35
  return f'{self.__class__.__name__}.{self.name}'
@@ -39,19 +39,25 @@ class BasicEnum(Enum):
39
39
 
40
40
 
41
41
  class IndexedEnum(Enum):
42
- __slots__: Tuple[str, ...] = ('index',)
42
+ __slots__: tuple[str, ...] = ('index',)
43
43
 
44
44
  index: int
45
45
  """The index value."""
46
46
 
47
- def __lt__(self, other: Union['IndexedEnum', int, float]) -> bool:
48
- return self.index < getattr(other, 'index', other)
47
+ def __lt__(self, other: Union['IndexedEnum', float, int]) -> bool:
48
+ return float(self.index) < float(other)
49
49
 
50
- def __eq__(self, other: Union['IndexedEnum', int, float]) -> bool:
51
- return self.index == getattr(other, 'index', other)
50
+ def __le__(self, other: Union['IndexedEnum', float, int]) -> bool:
51
+ return float(self.index) <= float(other)
52
52
 
53
- def __gt__(self, other: Union['IndexedEnum', int, float]) -> bool:
54
- return self.index > getattr(other, 'index', other)
53
+ def __eq__(self, other: Union['IndexedEnum', float, int]) -> bool:
54
+ return float(self.index) == float(other)
55
+
56
+ def __gt__(self, other: Union['IndexedEnum', float, int]) -> bool:
57
+ return float(self.index) > float(other)
58
+
59
+ def __ge__(self, other: Union['IndexedEnum', float, int]) -> bool:
60
+ return float(self.index) >= float(other)
55
61
 
56
62
  def __hash__(self) -> int:
57
63
  return self.index
@@ -59,25 +65,28 @@ class IndexedEnum(Enum):
59
65
  def __int__(self) -> int:
60
66
  return self.index
61
67
 
68
+ def __float__(self) -> float:
69
+ return float(self.index)
70
+
62
71
 
63
72
  class HeatIndex(IndexedEnum):
64
- """Represents a heat index."""
73
+ """A heat index."""
65
74
 
66
- __slots__: Tuple[str, ...] = ()
75
+ __slots__: tuple[str, ...] = ()
67
76
 
68
77
  CAUTION = None
69
78
  EXTREME_CAUTION = None
70
79
  DANGER = None
71
80
  EXTREME_DANGER = None
72
81
 
73
- def _new(celcius_index: int, true_index: int) -> Self:
82
+ def _new(celcius_index: int, true_index: int) -> 'HeatIndex':
74
83
  enum = HeatIndex(celcius_index)
75
84
  enum.index = true_index
76
85
 
77
86
  return enum
78
87
 
79
88
  @classmethod
80
- def _missing_(self, celcius_index: int) -> Self:
89
+ def _missing_(self, celcius_index: int) -> 'HeatIndex':
81
90
  if celcius_index <= 32:
82
91
  return self.CAUTION
83
92
  elif celcius_index <= 39:
@@ -89,9 +98,9 @@ class HeatIndex(IndexedEnum):
89
98
 
90
99
 
91
100
  class UltraViolet(BasicEnum, IndexedEnum):
92
- """Represents ultra-violet (UV) index."""
101
+ """An ultra-violet (UV) index."""
93
102
 
94
- __slots__: Tuple[str, ...] = ()
103
+ __slots__: tuple[str, ...] = ()
95
104
 
96
105
  LOW = None
97
106
  MODERATE = None
@@ -99,14 +108,14 @@ class UltraViolet(BasicEnum, IndexedEnum):
99
108
  VERY_HIGH = None
100
109
  EXTREME = None
101
110
 
102
- def _new(index: int) -> Self:
111
+ def _new(index: int) -> 'UltraViolet':
103
112
  enum = UltraViolet(index)
104
113
  enum.index = index
105
114
 
106
115
  return enum
107
116
 
108
117
  @classmethod
109
- def _missing_(self, index: int) -> Self:
118
+ def _missing_(self, index: int) -> 'UltraViolet':
110
119
  if index <= 2:
111
120
  return self.LOW
112
121
  elif index <= 5:
@@ -120,9 +129,9 @@ class UltraViolet(BasicEnum, IndexedEnum):
120
129
 
121
130
 
122
131
  class WindDirection(BasicEnum):
123
- """Represents a wind direction."""
132
+ """A wind direction."""
124
133
 
125
- __slots__: Tuple[str, ...] = ('degrees',)
134
+ __slots__: tuple[str, ...] = ('degrees',)
126
135
 
127
136
  NORTH = 'N'
128
137
  NORTH_NORTHEAST = 'NNE'
@@ -144,14 +153,14 @@ class WindDirection(BasicEnum):
144
153
  degrees: float
145
154
  """The wind direction's value in degrees."""
146
155
 
147
- def _new(value: str, degrees: float) -> Self:
156
+ def _new(value: str, degrees: float) -> 'WindDirection':
148
157
  enum = WindDirection(value)
149
158
  enum.degrees = degrees
150
159
 
151
160
  return enum
152
161
 
153
162
  def __contains__(self, other: Union['WindDirection', float, int]) -> bool:
154
- other = getattr(other, 'degrees', other)
163
+ other = float(other)
155
164
 
156
165
  if self is self.NORTH:
157
166
  return other > 348.75 or other <= 11.25
@@ -194,15 +203,15 @@ class WindDirection(BasicEnum):
194
203
 
195
204
  @property
196
205
  def emoji(self) -> str:
197
- """The emoji representing this enum."""
206
+ """Emoji representation."""
198
207
 
199
208
  return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360) / 45.0)]
200
209
 
201
210
 
202
211
  class Locale(Enum):
203
- """Represents the list of supported locales/languages by this library."""
212
+ """Supported locales/languages."""
204
213
 
205
- __slots__: Tuple[str, ...] = ()
214
+ __slots__: tuple[str, ...] = ()
206
215
 
207
216
  AFRIKAANS = 'af'
208
217
  AMHARIC = 'am'
@@ -286,9 +295,9 @@ class Locale(Enum):
286
295
 
287
296
 
288
297
  class Kind(BasicEnum):
289
- """Represents a weather forecast kind."""
298
+ """A weather forecast kind."""
290
299
 
291
- __slots__: Tuple[str, ...] = ()
300
+ __slots__: tuple[str, ...] = ()
292
301
 
293
302
  SUNNY = 113
294
303
  PARTLY_CLOUDY = 116
@@ -310,7 +319,7 @@ class Kind(BasicEnum):
310
319
  THUNDERY_SNOW_SHOWERS = 392
311
320
 
312
321
  @classmethod
313
- def _missing_(self, value: int) -> Self:
322
+ def _missing_(self, value: int) -> 'Kind':
314
323
  if value in (248, 260):
315
324
  return self.FOG
316
325
  elif value in (263, 353):
@@ -338,7 +347,7 @@ class Kind(BasicEnum):
338
347
 
339
348
  @property
340
349
  def emoji(self) -> str:
341
- """The emoji representing this enum."""
350
+ """Emoji representation."""
342
351
 
343
352
  if self is self.CLOUDY:
344
353
  return '☁️'
@@ -381,9 +390,9 @@ class Kind(BasicEnum):
381
390
 
382
391
 
383
392
  class Phase(BasicEnum):
384
- """Represents a moon phase."""
393
+ """A moon phase."""
385
394
 
386
- __slots__: Tuple[str, ...] = ()
395
+ __slots__: tuple[str, ...] = ()
387
396
 
388
397
  NEW_MOON = 'New Moon'
389
398
  WAXING_CRESCENT = 'Waxing Crescent'
@@ -396,7 +405,7 @@ class Phase(BasicEnum):
396
405
 
397
406
  @property
398
407
  def emoji(self) -> str:
399
- """The stylized name for this enum."""
408
+ """Emoji representation."""
400
409
 
401
410
  if self is self.NEW_MOON:
402
411
  return '🌑'
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,24 +22,27 @@ 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 typing import Tuple
25
+ from typing import Optional
26
26
 
27
27
 
28
28
  class Error(Exception):
29
- """Represents a ``python_weather`` error class. Extends :py:class:`Exception`."""
29
+ """The base error class. Extends :py:class:`Exception`."""
30
30
 
31
- __slots__: Tuple[str, ...] = ()
31
+ __slots__: tuple[str, ...] = ()
32
32
 
33
33
 
34
34
  class RequestError(Error):
35
- """Thrown upon HTTP request failure. Extends :class:`~python_weather.errors.Error`."""
35
+ """Thrown upon HTTP request failure. Extends :class:`.Error`."""
36
36
 
37
- __slots__: Tuple[str, ...] = ('source',)
37
+ __slots__: tuple[str, ...] = ('status',)
38
38
 
39
- source: Exception
40
- """The :py:class:`Exception` instance causing this exception."""
39
+ status: Optional[int]
40
+ """The status code returned from the API."""
41
41
 
42
- def __init__(self, source: Exception):
43
- self.source = source
42
+ def __init__(self, status: Optional[int]):
43
+ self.status = status
44
44
 
45
45
  super().__init__()
46
+
47
+ def __repr__(self) -> str:
48
+ return f'<{__class__.__name__} status={self.status}>'
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The MIT License (MIT)
3
3
 
4
- Copyright (c) 2021-2024 null8626
4
+ Copyright (c) 2021-2025 null8626
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,18 +22,19 @@ 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 typing import Iterable, Optional, Tuple, List
26
25
  from datetime import datetime, date, time
26
+ from collections.abc import Iterable
27
+ from typing import Optional
27
28
 
28
- from .base import BaseForecast
29
29
  from .enums import Phase, Locale, HeatIndex
30
30
  from .constants import _Unit, LATLON_REGEX
31
+ from .base import BaseForecast
31
32
 
32
33
 
33
34
  class HourlyForecast(BaseForecast):
34
- """Represents a weather forecast of a specific hour."""
35
+ """A weather forecast for a specific hour."""
35
36
 
36
- __slots__: Tuple[str, ...] = (
37
+ __slots__: tuple[str, ...] = (
37
38
  'chances_of_fog',
38
39
  'chances_of_frost',
39
40
  'chances_of_high_temperature',
@@ -101,9 +102,10 @@ class HourlyForecast(BaseForecast):
101
102
  """The wind gust value in either kilometers/hour or miles/hour."""
102
103
 
103
104
  def __init__(self, json: dict, unit: _Unit, locale: Locale):
104
- # for inheritance purposes
105
+ # For inheritance purposes.
105
106
  if 'temp_C' not in json:
106
107
  json['temp_C'] = json.pop('tempC')
108
+
107
109
  if 'temp_F' not in json:
108
110
  json['temp_F'] = json.pop('tempF')
109
111
 
@@ -133,11 +135,13 @@ class HourlyForecast(BaseForecast):
133
135
  super().__init__(json, unit, locale)
134
136
 
135
137
  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}>'
138
+ return f'<{__class__.__name__} time={self.time!r} temperature={self.temperature} description={self.description!r} kind={self.kind!r}>'
137
139
 
138
140
 
139
141
  class DailyForecast:
140
- __slots__: Tuple[str, ...] = (
142
+ """A weather forecast for a specific day."""
143
+
144
+ __slots__: tuple[str, ...] = (
141
145
  'moon_illumination',
142
146
  'moon_phase',
143
147
  'moonrise',
@@ -160,16 +164,16 @@ class DailyForecast:
160
164
  """The moon's phase."""
161
165
 
162
166
  moonrise: Optional[time]
163
- """The local time when the moon rises. This can be ``None``."""
167
+ """The local time when the moon rises."""
164
168
 
165
169
  moonset: Optional[time]
166
- """The local time when the moon sets. This can be ``None``."""
170
+ """The local time when the moon sets."""
167
171
 
168
172
  sunrise: Optional[time]
169
- """The local time when the sun rises. This can be ``None``."""
173
+ """The local time when the sun rises."""
170
174
 
171
175
  sunset: Optional[time]
172
- """The local time when the sun sets. This can be ``None``."""
176
+ """The local time when the sun sets."""
173
177
 
174
178
  date: 'date'
175
179
  """The local date of this forecast."""
@@ -189,7 +193,7 @@ class DailyForecast:
189
193
  snowfall: float
190
194
  """Total snowfall in either centimeters or inches."""
191
195
 
192
- hourly_forecasts: List[HourlyForecast]
196
+ hourly_forecasts: list[HourlyForecast]
193
197
  """The hourly forecasts of this day."""
194
198
 
195
199
  def __init__(self, json: dict, unit: _Unit, locale: Locale):
@@ -201,7 +205,7 @@ class DailyForecast:
201
205
  self.moonset = __class__.__parse_time(astronomy['moonset'])
202
206
  self.sunrise = __class__.__parse_time(astronomy['sunrise'])
203
207
  self.sunset = __class__.__parse_time(astronomy['sunset'])
204
- self.date = datetime.strptime(json['date'], '%Y-%m-%d').date()
208
+ self.date = date.fromisoformat(json['date'])
205
209
  self.sunlight = float(json['sunHour'])
206
210
  self.lowest_temperature = int(json[f'mintemp{unit.temperature}'])
207
211
  self.highest_temperature = int(json[f'maxtemp{unit.temperature}'])
@@ -219,7 +223,7 @@ class DailyForecast:
219
223
  ...
220
224
 
221
225
  def __repr__(self) -> str:
222
- return f'<{__class__.__name__} date={self.date!r} temperature={self.temperature!r}>'
226
+ return f'<{__class__.__name__} date={self.date!r} temperature={self.temperature}>'
223
227
 
224
228
  def __len__(self) -> int:
225
229
  return len(self.hourly_forecasts)
@@ -229,9 +233,9 @@ class DailyForecast:
229
233
 
230
234
 
231
235
  class Forecast(BaseForecast):
232
- """Represents today's weather forecast, alongside daily and hourly weather forecasts."""
236
+ """A set of weather forecasts for a certain location."""
233
237
 
234
- __slots__: Tuple[str, ...] = (
238
+ __slots__: tuple[str, ...] = (
235
239
  'local_population',
236
240
  'region',
237
241
  'location',
@@ -256,10 +260,10 @@ class Forecast(BaseForecast):
256
260
  datetime: 'datetime'
257
261
  """The local date and time of this weather forecast."""
258
262
 
259
- coordinates: Tuple[float, float]
260
- """A tuple of this forecast's latitude and longitude."""
263
+ coordinates: tuple[float, float]
264
+ """This forecast's latitude and longitude."""
261
265
 
262
- daily_forecasts: List[DailyForecast]
266
+ daily_forecasts: list[DailyForecast]
263
267
  """Daily weather forecasts in this location."""
264
268
 
265
269
  def __init__(self, json: dict, unit: _Unit, locale: Locale):
@@ -277,7 +281,7 @@ class Forecast(BaseForecast):
277
281
  match = LATLON_REGEX.match(req['query'])
278
282
 
279
283
  self.coordinates = (float(match[1]), float(match[2]))
280
- except:
284
+ except (KeyError, IndexError, StopIteration):
281
285
  self.coordinates = (float(nearest['latitude']), float(nearest['longitude']))
282
286
 
283
287
  self.daily_forecasts = [
@@ -287,7 +291,7 @@ class Forecast(BaseForecast):
287
291
  super().__init__(current, unit, locale)
288
292
 
289
293
  def __repr__(self) -> str:
290
- return f'<{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature!r}>'
294
+ return f'<{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature}>'
291
295
 
292
296
  def __len__(self) -> int:
293
297
  return len(self.daily_forecasts)
@@ -0,0 +1 @@
1
+ VERSION = '3.0.0'
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-weather
3
- Version: 2.0.6
3
+ Version: 2.1.0
4
4
  Summary: A free and asynchronous weather API wrapper made in Python, for Python.
5
5
  Author: null8626
6
6
  License: MIT
@@ -26,16 +26,16 @@ Classifier: License :: OSI Approved :: MIT License
26
26
  Classifier: Programming Language :: Python
27
27
  Classifier: Programming Language :: Python :: 3
28
28
  Classifier: Programming Language :: Python :: 3 :: Only
29
- Classifier: Programming Language :: Python :: 3.8
30
29
  Classifier: Programming Language :: Python :: 3.9
31
30
  Classifier: Programming Language :: Python :: 3.10
32
31
  Classifier: Programming Language :: Python :: 3.11
33
32
  Classifier: Programming Language :: Python :: 3.12
34
33
  Classifier: Programming Language :: Python :: 3.13
35
- Requires-Python: >=3.8
34
+ Requires-Python: >=3.9
36
35
  Description-Content-Type: text/markdown
37
36
  License-File: LICENSE
38
- Requires-Dist: aiohttp>=3.10.10
37
+ Requires-Dist: aiohttp>=3.12.4
38
+ Dynamic: license-file
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
 
@@ -50,10 +50,12 @@ Requires-Dist: aiohttp>=3.10.10
50
50
 
51
51
  A free and asynchronous weather Python API wrapper made in Python, for Python.
52
52
 
53
- ## Installation
53
+ ## Getting started
54
+
55
+ Run the following command in your terminal:
54
56
 
55
57
  ```console
56
- pip install python-weather
58
+ $ pip install python-weather
57
59
  ```
58
60
 
59
61
  ## Example
@@ -61,36 +63,40 @@ pip install python-weather
61
63
  For more information, please read the [documentation](https://python-weather.readthedocs.io/en/latest/).
62
64
 
63
65
  ```py
64
- # import the module
66
+ # Import the module.
65
67
  import python_weather
66
68
 
67
69
  import asyncio
68
70
  import os
69
71
 
70
- async def getweather() -> None:
71
- # declare the client. the measuring unit used defaults to the metric system (celcius, km/h, etc.)
72
+
73
+ async def main() -> None:
74
+
75
+ # Declare the client. The measuring unit used defaults to the metric system (celcius, km/h, etc.)
72
76
  async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
73
- # fetch a weather forecast from a city
77
+
78
+ # Fetch a weather forecast from a city.
74
79
  weather = await client.get('New York')
75
80
 
76
- # returns the current day's forecast temperature (int)
81
+ # Fetch the temperature for today.
77
82
  print(weather.temperature)
78
83
 
79
- # get the weather forecast for a few days
84
+ # Fetch weather forecast for upcoming days.
80
85
  for daily in weather:
81
86
  print(daily)
82
-
83
- # hourly forecasts
87
+
88
+ # Each daily forecast has their own hourly forecasts.
84
89
  for hourly in daily:
85
90
  print(f' --> {hourly!r}')
86
91
 
87
92
  if __name__ == '__main__':
88
- # see https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
89
- # for more details
93
+
94
+ # See https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
95
+ # for more details.
90
96
  if os.name == 'nt':
91
97
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
92
98
 
93
- asyncio.run(getweather())
99
+ asyncio.run(main())
94
100
  ```
95
101
 
96
102
  ## Data source
@@ -9,6 +9,7 @@ python_weather/constants.py
9
9
  python_weather/enums.py
10
10
  python_weather/errors.py
11
11
  python_weather/forecast.py
12
+ python_weather/version.py
12
13
  python_weather.egg-info/PKG-INFO
13
14
  python_weather.egg-info/SOURCES.txt
14
15
  python_weather.egg-info/dependency_links.txt
@@ -0,0 +1 @@
1
+ aiohttp>=3.12.4
@@ -1 +0,0 @@
1
- aiohttp>=3.10.10
File without changes