python-weather 2.1.0__py3-none-any.whl → 2.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_weather/__init__.py +8 -24
- python_weather/base.py +15 -28
- python_weather/client.py +58 -42
- python_weather/constants.py +33 -27
- python_weather/enums.py +64 -156
- python_weather/errors.py +13 -32
- python_weather/forecast.py +30 -49
- python_weather/version.py +4 -1
- {python_weather-2.1.0.dist-info → python_weather-2.1.2.dist-info}/METADATA +104 -110
- python_weather-2.1.2.dist-info/RECORD +13 -0
- {python_weather-2.1.0.dist-info → python_weather-2.1.2.dist-info}/WHEEL +1 -1
- {python_weather-2.1.0.dist-info → python_weather-2.1.2.dist-info}/licenses/LICENSE +1 -1
- python_weather-2.1.0.dist-info/RECORD +0 -13
- {python_weather-2.1.0.dist-info → python_weather-2.1.2.dist-info}/top_level.txt +0 -0
python_weather/__init__.py
CHANGED
|
@@ -1,44 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 .enums import HeatIndex, Kind, Locale, Phase, UltraViolet, WindDirection
|
|
26
5
|
from .constants import METRIC, IMPERIAL
|
|
27
6
|
from .errors import Error, RequestError
|
|
7
|
+
from .forecast import Forecast
|
|
28
8
|
from .version import VERSION
|
|
29
9
|
from .client import Client
|
|
30
10
|
|
|
31
11
|
|
|
32
12
|
__title__ = 'python-weather'
|
|
33
13
|
__author__ = 'null8626'
|
|
14
|
+
__credits__ = (__author__,)
|
|
15
|
+
__maintainer__ = __author__
|
|
16
|
+
__status__ = 'Production'
|
|
34
17
|
__license__ = 'MIT'
|
|
35
|
-
__copyright__ = 'Copyright (c) 2021-
|
|
18
|
+
__copyright__ = 'Copyright (c) 2021-2026 null8626'
|
|
36
19
|
__version__ = VERSION
|
|
37
20
|
__all__ = (
|
|
38
21
|
'METRIC',
|
|
39
22
|
'IMPERIAL',
|
|
40
23
|
'Client',
|
|
41
24
|
'Error',
|
|
25
|
+
'Forecast',
|
|
42
26
|
'RequestError',
|
|
43
27
|
'HeatIndex',
|
|
44
28
|
'Kind',
|
python_weather/base.py
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 .enums import WindDirection, Kind, Locale, UltraViolet
|
|
26
5
|
from .constants import _Unit
|
|
@@ -28,7 +7,10 @@ from .errors import Error
|
|
|
28
7
|
|
|
29
8
|
|
|
30
9
|
class CustomizableBase:
|
|
31
|
-
__slots__: tuple[str, ...] =
|
|
10
|
+
__slots__: tuple[str, ...] = '_unit', '_locale'
|
|
11
|
+
|
|
12
|
+
_unit: _Unit
|
|
13
|
+
_locale: Locale
|
|
32
14
|
|
|
33
15
|
def __init__(self, unit: _Unit, locale: Locale):
|
|
34
16
|
self.unit = unit
|
|
@@ -38,7 +20,7 @@ class CustomizableBase:
|
|
|
38
20
|
def unit(self) -> _Unit:
|
|
39
21
|
"""The measuring unit used."""
|
|
40
22
|
|
|
41
|
-
return self.
|
|
23
|
+
return self._unit
|
|
42
24
|
|
|
43
25
|
@unit.setter
|
|
44
26
|
def unit(self, to: _Unit) -> None:
|
|
@@ -53,13 +35,13 @@ class CustomizableBase:
|
|
|
53
35
|
if not isinstance(to, _Unit):
|
|
54
36
|
raise Error('Invalid measuring unit specified!')
|
|
55
37
|
|
|
56
|
-
self.
|
|
38
|
+
self._unit = to
|
|
57
39
|
|
|
58
40
|
@property
|
|
59
41
|
def locale(self) -> Locale:
|
|
60
42
|
"""The localization used."""
|
|
61
43
|
|
|
62
|
-
return self.
|
|
44
|
+
return self._locale
|
|
63
45
|
|
|
64
46
|
@locale.setter
|
|
65
47
|
def locale(self, to: Locale) -> None:
|
|
@@ -75,11 +57,12 @@ class CustomizableBase:
|
|
|
75
57
|
if not isinstance(to, Locale):
|
|
76
58
|
raise Error(f'Expected {to!r} to be a Locale enum')
|
|
77
59
|
|
|
78
|
-
self.
|
|
60
|
+
self._locale = to
|
|
79
61
|
|
|
80
62
|
|
|
81
63
|
class BaseForecast:
|
|
82
64
|
__slots__: tuple[str, ...] = (
|
|
65
|
+
'cloud_cover',
|
|
83
66
|
'ultraviolet',
|
|
84
67
|
'humidity',
|
|
85
68
|
'wind_direction',
|
|
@@ -93,6 +76,9 @@ class BaseForecast:
|
|
|
93
76
|
'description',
|
|
94
77
|
)
|
|
95
78
|
|
|
79
|
+
cloud_cover: int
|
|
80
|
+
"""The cloud cover value in percent."""
|
|
81
|
+
|
|
96
82
|
ultraviolet: UltraViolet
|
|
97
83
|
"""The ultra-violet index."""
|
|
98
84
|
|
|
@@ -133,6 +119,7 @@ class BaseForecast:
|
|
|
133
119
|
else json[f'lang_{locale.value}'][0]['value']
|
|
134
120
|
)
|
|
135
121
|
|
|
122
|
+
self.cloud_cover = int(json['cloudcover'])
|
|
136
123
|
self.ultraviolet = UltraViolet._new(int(json['uvIndex']))
|
|
137
124
|
self.humidity = int(json['humidity'])
|
|
138
125
|
self.wind_direction = WindDirection._new(
|
python_weather/client.py
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
27
|
-
from typing import Optional
|
|
28
6
|
from asyncio import sleep
|
|
29
7
|
|
|
30
8
|
from .errors import Error, RequestError
|
|
@@ -39,56 +17,82 @@ class Client(CustomizableBase):
|
|
|
39
17
|
"""
|
|
40
18
|
Interact with the API's endpoints.
|
|
41
19
|
|
|
20
|
+
Examples:
|
|
21
|
+
|
|
22
|
+
.. code-block:: python
|
|
23
|
+
|
|
24
|
+
# Explicit cleanup
|
|
25
|
+
client = python_weather.Client(unit=python_weather.IMPERIAL)
|
|
26
|
+
|
|
27
|
+
# ...
|
|
28
|
+
|
|
29
|
+
await client.close()
|
|
30
|
+
|
|
31
|
+
# Implicit cleanup
|
|
32
|
+
async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
|
|
33
|
+
# ...
|
|
34
|
+
|
|
42
35
|
:param unit: Whether to use the metric or imperial/customary system (:data:`~.constants.IMPERIAL`). Defaults to :data:`~.constants.METRIC`.
|
|
43
36
|
:type unit: ``_Unit``
|
|
44
37
|
:param locale: Whether to use a different locale/language as the description for the returned forecast. Defaults to :attr:`.Locale.ENGLISH`.
|
|
45
38
|
:type locale: :class:`.Locale`
|
|
46
39
|
:param session: Whether to use an existing :class:`~aiohttp.ClientSession` for requesting or not. Defaults to :py:obj:`None` (creates a new one instead).
|
|
47
|
-
:type session:
|
|
48
|
-
:param max_retries: Maximum amount of retries upon request failure before raising a :class:`.RequestError`.
|
|
49
|
-
|
|
40
|
+
:type session: :class:`~aiohttp.ClientSession` | :py:obj:`None`
|
|
41
|
+
:param max_retries: Maximum amount of retries upon request failure before raising a :class:`.RequestError`.
|
|
42
|
+
Use ``-1`` to disable (NOT recommended). Defaults to 3 retries.
|
|
43
|
+
:type max_retries: :class:`int` | :py:obj:`None`
|
|
50
44
|
|
|
51
45
|
:exception Error: ``unit`` is not :data:`~.constants.METRIC` or :data:`~.constants.IMPERIAL` or ``locale`` is not a part of the :class:`.Locale` enum.
|
|
52
46
|
"""
|
|
53
47
|
|
|
54
|
-
__slots__: tuple[str, ...] =
|
|
48
|
+
__slots__: tuple[str, ...] = '__own_session', '__session', '_max_retries'
|
|
49
|
+
|
|
50
|
+
__own_session: bool
|
|
51
|
+
__session: ClientSession
|
|
52
|
+
_max_retries: int
|
|
55
53
|
|
|
56
54
|
def __init__(
|
|
57
55
|
self,
|
|
58
56
|
*,
|
|
59
57
|
unit: _Unit = METRIC,
|
|
60
58
|
locale: Locale = Locale.ENGLISH,
|
|
61
|
-
session:
|
|
62
|
-
max_retries:
|
|
59
|
+
session: ClientSession | None = None,
|
|
60
|
+
max_retries: int | None = None,
|
|
63
61
|
):
|
|
64
62
|
super().__init__(unit, locale)
|
|
65
63
|
|
|
66
64
|
self.__own_session = session is None
|
|
67
65
|
self.__session = session or ClientSession(
|
|
68
66
|
timeout=ClientTimeout(total=5000.0),
|
|
69
|
-
connector=TCPConnector(
|
|
67
|
+
connector=TCPConnector(ssl=False),
|
|
70
68
|
)
|
|
71
|
-
self.
|
|
69
|
+
self._max_retries = max_retries or 3
|
|
72
70
|
|
|
73
71
|
def __repr__(self) -> str:
|
|
74
|
-
return f'<{__class__.__name__} {self.__session!r}>'
|
|
72
|
+
return f'<{__class__.__module__}.{__class__.__name__} {self.__session!r}>'
|
|
75
73
|
|
|
76
74
|
async def get(
|
|
77
75
|
self,
|
|
78
76
|
location: str,
|
|
79
77
|
*,
|
|
80
|
-
unit:
|
|
81
|
-
locale:
|
|
78
|
+
unit: _Unit | None = None,
|
|
79
|
+
locale: Locale | None = None,
|
|
82
80
|
) -> Forecast:
|
|
83
81
|
"""
|
|
84
82
|
Fetches a weather forecast for a specific location.
|
|
85
83
|
|
|
84
|
+
Example:
|
|
85
|
+
|
|
86
|
+
.. code-block:: python
|
|
87
|
+
|
|
88
|
+
weather = await client.get('New York')
|
|
89
|
+
|
|
86
90
|
:param location: The requested location.
|
|
87
91
|
:type location: :py:class:`str`
|
|
88
92
|
:param unit: Overrides the unit used.
|
|
89
|
-
:type unit:
|
|
93
|
+
:type unit: ``_Unit`` | :py:obj:`None`
|
|
90
94
|
:param locale: Overrides the locale used.
|
|
91
|
-
:type locale:
|
|
95
|
+
:type locale: :class:`.Locale` | :py:obj:`None`
|
|
92
96
|
|
|
93
97
|
:exception TypeError: ``location`` is not a :py:class:`str` or is empty.
|
|
94
98
|
:exception Error: The client is already closed.
|
|
@@ -105,14 +109,16 @@ class Client(CustomizableBase):
|
|
|
105
109
|
raise TypeError(f'Expected a proper location str, got {location!r}')
|
|
106
110
|
|
|
107
111
|
if not isinstance(unit, _Unit):
|
|
108
|
-
unit = self.
|
|
112
|
+
unit = self._unit
|
|
109
113
|
|
|
110
114
|
if not isinstance(locale, Locale):
|
|
111
|
-
locale = self.
|
|
115
|
+
locale = self._locale
|
|
112
116
|
|
|
113
117
|
subdomain = f'{locale.value}.' if locale != Locale.ENGLISH else ''
|
|
114
118
|
attempts = 0
|
|
119
|
+
|
|
115
120
|
status = None
|
|
121
|
+
reason = None
|
|
116
122
|
|
|
117
123
|
while True:
|
|
118
124
|
try:
|
|
@@ -124,18 +130,28 @@ class Client(CustomizableBase):
|
|
|
124
130
|
},
|
|
125
131
|
) as resp:
|
|
126
132
|
status = resp.status
|
|
133
|
+
reason = resp.reason
|
|
134
|
+
|
|
127
135
|
resp.raise_for_status()
|
|
128
136
|
|
|
129
137
|
return Forecast(await resp.json(), unit, locale)
|
|
130
138
|
except ClientResponseError:
|
|
131
|
-
if attempts == self.
|
|
132
|
-
raise RequestError(status) from None
|
|
139
|
+
if attempts == self._max_retries:
|
|
140
|
+
raise RequestError(status, reason) from None
|
|
133
141
|
|
|
134
142
|
await sleep(0.5 * (2**attempts))
|
|
135
143
|
attempts += 1
|
|
136
144
|
|
|
137
145
|
async def close(self) -> None:
|
|
138
|
-
"""
|
|
146
|
+
"""
|
|
147
|
+
Closes the :class:`.Client` object.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
|
|
151
|
+
.. code-block:: python
|
|
152
|
+
|
|
153
|
+
await client.close()
|
|
154
|
+
"""
|
|
139
155
|
|
|
140
156
|
if self.__own_session and not self.__session.closed:
|
|
141
157
|
await self.__session.close()
|
python_weather/constants.py
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 typing import Union
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: 2021-2026 null8626
|
|
3
|
+
|
|
26
4
|
import re
|
|
27
5
|
|
|
28
6
|
|
|
@@ -36,6 +14,13 @@ class _Unit:
|
|
|
36
14
|
'cm_divisor',
|
|
37
15
|
)
|
|
38
16
|
|
|
17
|
+
temperature: str
|
|
18
|
+
velocity: str
|
|
19
|
+
pressure: str
|
|
20
|
+
precipitation: str
|
|
21
|
+
visibility: str
|
|
22
|
+
cm_divisor: float | int
|
|
23
|
+
|
|
39
24
|
def __init__(
|
|
40
25
|
self,
|
|
41
26
|
temperature: str,
|
|
@@ -43,7 +28,7 @@ class _Unit:
|
|
|
43
28
|
pressure: str,
|
|
44
29
|
precipitation: str,
|
|
45
30
|
visibility: str,
|
|
46
|
-
cm_divisor:
|
|
31
|
+
cm_divisor: float | int,
|
|
47
32
|
):
|
|
48
33
|
self.temperature = temperature
|
|
49
34
|
self.velocity = velocity
|
|
@@ -59,5 +44,26 @@ class _Unit:
|
|
|
59
44
|
METRIC = _Unit('C', 'Kmph', '', 'MM', '', 1)
|
|
60
45
|
IMPERIAL = _Unit('F', 'Miles', 'Inches', 'Inches', 'Miles', 2.54)
|
|
61
46
|
|
|
62
|
-
WIND_DIRECTION_EMOJIS = ('↓', '↙', '←', '↖', '↑', '↗', '→', '↘')
|
|
63
47
|
LATLON_REGEX = re.compile(r'^Lat (\-?[\d\.]+) and Lon (\-?[\d\.]+)$')
|
|
48
|
+
|
|
49
|
+
KIND_EMOJIS = (
|
|
50
|
+
'☀️',
|
|
51
|
+
'⛅️',
|
|
52
|
+
'☁️',
|
|
53
|
+
'☁️',
|
|
54
|
+
'🌫',
|
|
55
|
+
'🌦',
|
|
56
|
+
'🌧',
|
|
57
|
+
'🌧',
|
|
58
|
+
'⛈',
|
|
59
|
+
'🌨',
|
|
60
|
+
'❄️',
|
|
61
|
+
'🌦',
|
|
62
|
+
'🌧',
|
|
63
|
+
'🌧',
|
|
64
|
+
'🌨',
|
|
65
|
+
'❄️',
|
|
66
|
+
'🌩',
|
|
67
|
+
'⛈',
|
|
68
|
+
)
|
|
69
|
+
WIND_DIRECTION_EMOJIS = '↑', '↖', '←', '↙', '↓', '↘', '→', '↗'
|
python_weather/enums.py
CHANGED
|
@@ -1,31 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 typing import Union
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: 2021-2026 null8626
|
|
3
|
+
|
|
26
4
|
from enum import Enum
|
|
27
5
|
|
|
28
|
-
from .constants import WIND_DIRECTION_EMOJIS
|
|
6
|
+
from .constants import KIND_EMOJIS, WIND_DIRECTION_EMOJIS
|
|
29
7
|
|
|
30
8
|
|
|
31
9
|
class BasicEnum(Enum):
|
|
@@ -37,6 +15,10 @@ class BasicEnum(Enum):
|
|
|
37
15
|
def __str__(self) -> str:
|
|
38
16
|
return self.name.replace('_', ' ').title()
|
|
39
17
|
|
|
18
|
+
@property
|
|
19
|
+
def _index(self) -> int:
|
|
20
|
+
return next(filter(lambda kind: self is kind[1], enumerate(self.__class__)))[0]
|
|
21
|
+
|
|
40
22
|
|
|
41
23
|
class IndexedEnum(Enum):
|
|
42
24
|
__slots__: tuple[str, ...] = ('index',)
|
|
@@ -44,19 +26,22 @@ class IndexedEnum(Enum):
|
|
|
44
26
|
index: int
|
|
45
27
|
"""The index value."""
|
|
46
28
|
|
|
47
|
-
def __lt__(self, other:
|
|
29
|
+
def __lt__(self, other: 'IndexedEnum | float | int') -> bool:
|
|
48
30
|
return float(self.index) < float(other)
|
|
49
31
|
|
|
50
|
-
def __le__(self, other:
|
|
32
|
+
def __le__(self, other: 'IndexedEnum | float | int') -> bool:
|
|
51
33
|
return float(self.index) <= float(other)
|
|
52
34
|
|
|
53
|
-
def __eq__(self, other:
|
|
54
|
-
|
|
35
|
+
def __eq__(self, other: object) -> bool:
|
|
36
|
+
if other_float := getattr(other, '__float__', None):
|
|
37
|
+
return float(self.index) == other_float()
|
|
55
38
|
|
|
56
|
-
|
|
39
|
+
return False # pragma: nocover
|
|
40
|
+
|
|
41
|
+
def __gt__(self, other: 'IndexedEnum | float | int') -> bool:
|
|
57
42
|
return float(self.index) > float(other)
|
|
58
43
|
|
|
59
|
-
def __ge__(self, other:
|
|
44
|
+
def __ge__(self, other: 'IndexedEnum | float | int') -> bool:
|
|
60
45
|
return float(self.index) >= float(other)
|
|
61
46
|
|
|
62
47
|
def __hash__(self) -> int:
|
|
@@ -79,6 +64,7 @@ class HeatIndex(IndexedEnum):
|
|
|
79
64
|
DANGER = None
|
|
80
65
|
EXTREME_DANGER = None
|
|
81
66
|
|
|
67
|
+
@staticmethod
|
|
82
68
|
def _new(celcius_index: int, true_index: int) -> 'HeatIndex':
|
|
83
69
|
enum = HeatIndex(celcius_index)
|
|
84
70
|
enum.index = true_index
|
|
@@ -86,15 +72,16 @@ class HeatIndex(IndexedEnum):
|
|
|
86
72
|
return enum
|
|
87
73
|
|
|
88
74
|
@classmethod
|
|
89
|
-
def _missing_(
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
75
|
+
def _missing_(cls, value: object) -> 'HeatIndex | None':
|
|
76
|
+
if isinstance(value, int):
|
|
77
|
+
if value <= 32:
|
|
78
|
+
return cls.CAUTION
|
|
79
|
+
elif value <= 39:
|
|
80
|
+
return cls.EXTREME_CAUTION
|
|
81
|
+
elif value <= 51:
|
|
82
|
+
return cls.DANGER
|
|
83
|
+
else:
|
|
84
|
+
return cls.EXTREME_DANGER
|
|
98
85
|
|
|
99
86
|
|
|
100
87
|
class UltraViolet(BasicEnum, IndexedEnum):
|
|
@@ -108,6 +95,7 @@ class UltraViolet(BasicEnum, IndexedEnum):
|
|
|
108
95
|
VERY_HIGH = None
|
|
109
96
|
EXTREME = None
|
|
110
97
|
|
|
98
|
+
@staticmethod
|
|
111
99
|
def _new(index: int) -> 'UltraViolet':
|
|
112
100
|
enum = UltraViolet(index)
|
|
113
101
|
enum.index = index
|
|
@@ -115,17 +103,18 @@ class UltraViolet(BasicEnum, IndexedEnum):
|
|
|
115
103
|
return enum
|
|
116
104
|
|
|
117
105
|
@classmethod
|
|
118
|
-
def _missing_(
|
|
119
|
-
if
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
106
|
+
def _missing_(cls, value: object) -> 'UltraViolet | None':
|
|
107
|
+
if isinstance(value, int):
|
|
108
|
+
if value <= 2:
|
|
109
|
+
return cls.LOW
|
|
110
|
+
elif value <= 5:
|
|
111
|
+
return cls.MODERATE
|
|
112
|
+
elif value <= 7:
|
|
113
|
+
return cls.HIGH
|
|
114
|
+
elif value <= 10:
|
|
115
|
+
return cls.VERY_HIGH
|
|
116
|
+
else:
|
|
117
|
+
return cls.EXTREME
|
|
129
118
|
|
|
130
119
|
|
|
131
120
|
class WindDirection(BasicEnum):
|
|
@@ -153,47 +142,17 @@ class WindDirection(BasicEnum):
|
|
|
153
142
|
degrees: float
|
|
154
143
|
"""The wind direction's value in degrees."""
|
|
155
144
|
|
|
145
|
+
@staticmethod
|
|
156
146
|
def _new(value: str, degrees: float) -> 'WindDirection':
|
|
157
147
|
enum = WindDirection(value)
|
|
158
148
|
enum.degrees = degrees
|
|
159
149
|
|
|
160
150
|
return enum
|
|
161
151
|
|
|
162
|
-
def __contains__(self, other:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return other > 348.75 or other <= 11.25
|
|
167
|
-
elif self is self.NORTH_NORTHEAST:
|
|
168
|
-
return 11.25 < other <= 33.75
|
|
169
|
-
elif self is self.NORTHEAST:
|
|
170
|
-
return 33.75 < other <= 56.25
|
|
171
|
-
elif self is self.EAST_NORTHEAST:
|
|
172
|
-
return 56.25 < other <= 78.75
|
|
173
|
-
elif self is self.EAST:
|
|
174
|
-
return 78.75 < other <= 101.25
|
|
175
|
-
elif self is self.EAST_SOUTHEAST:
|
|
176
|
-
return 101.25 < other <= 123.75
|
|
177
|
-
elif self is self.SOUTHEAST:
|
|
178
|
-
return 123.75 < other <= 146.25
|
|
179
|
-
elif self is self.SOUTH_SOUTHEAST:
|
|
180
|
-
return 146.25 < other <= 168.75
|
|
181
|
-
elif self is self.SOUTH:
|
|
182
|
-
return 168.75 < other <= 191.25
|
|
183
|
-
elif self is self.SOUTH_SOUTHWEST:
|
|
184
|
-
return 191.25 < other <= 213.75
|
|
185
|
-
elif self is self.SOUTHWEST:
|
|
186
|
-
return 213.75 < other <= 236.25
|
|
187
|
-
elif self is self.WEST_SOUTHWEST:
|
|
188
|
-
return 236.25 < other <= 258.75
|
|
189
|
-
elif self is self.WEST:
|
|
190
|
-
return 258.75 < other <= 281.25
|
|
191
|
-
elif self is self.WEST_NORTHWEST:
|
|
192
|
-
return 281.25 < other <= 303.75
|
|
193
|
-
elif self is self.NORTHWEST:
|
|
194
|
-
return 303.75 < other <= 326.25
|
|
195
|
-
else:
|
|
196
|
-
return 326.25 < other <= 348.75
|
|
152
|
+
def __contains__(self, other: 'WindDirection | float | int') -> bool:
|
|
153
|
+
members = list(self.__class__)
|
|
154
|
+
|
|
155
|
+
return self is members[int(((float(other) % 360) + 11.25) // 22.5) % len(members)]
|
|
197
156
|
|
|
198
157
|
def __int__(self) -> int:
|
|
199
158
|
return int(self.degrees)
|
|
@@ -205,7 +164,7 @@ class WindDirection(BasicEnum):
|
|
|
205
164
|
def emoji(self) -> str:
|
|
206
165
|
"""Emoji representation."""
|
|
207
166
|
|
|
208
|
-
return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360)
|
|
167
|
+
return WIND_DIRECTION_EMOJIS[int(((self.degrees + 22.5) % 360) // 45)]
|
|
209
168
|
|
|
210
169
|
|
|
211
170
|
class Locale(Enum):
|
|
@@ -291,7 +250,8 @@ class Locale(Enum):
|
|
|
291
250
|
|
|
292
251
|
def __str__(self) -> str:
|
|
293
252
|
arr = self.name.title().split('_')
|
|
294
|
-
|
|
253
|
+
|
|
254
|
+
return f'{" ".join(arr[:-1])} ({arr[-1]})' if len(arr) != 1 else arr[0]
|
|
295
255
|
|
|
296
256
|
|
|
297
257
|
class Kind(BasicEnum):
|
|
@@ -319,74 +279,37 @@ class Kind(BasicEnum):
|
|
|
319
279
|
THUNDERY_SNOW_SHOWERS = 392
|
|
320
280
|
|
|
321
281
|
@classmethod
|
|
322
|
-
def _missing_(
|
|
282
|
+
def _missing_(cls, value: object) -> 'Kind | None':
|
|
323
283
|
if value in (248, 260):
|
|
324
|
-
return
|
|
284
|
+
return cls.FOG
|
|
325
285
|
elif value in (263, 353):
|
|
326
|
-
return
|
|
286
|
+
return cls.LIGHT_SHOWERS
|
|
327
287
|
elif value in (362, 365, 374):
|
|
328
|
-
return
|
|
288
|
+
return cls.LIGHT_SLEET_SHOWERS
|
|
329
289
|
elif value in (185, 281, 284, 311, 314, 317, 350, 377):
|
|
330
|
-
return
|
|
290
|
+
return cls.LIGHT_SLEET
|
|
331
291
|
elif value == 386:
|
|
332
|
-
return
|
|
292
|
+
return cls.THUNDERY_SHOWERS
|
|
333
293
|
elif value == 320:
|
|
334
|
-
return
|
|
294
|
+
return cls.LIGHT_SNOW
|
|
335
295
|
elif value in (329, 332, 338):
|
|
336
|
-
return
|
|
296
|
+
return cls.HEAVY_SNOW
|
|
337
297
|
elif value in (293, 296):
|
|
338
|
-
return
|
|
298
|
+
return cls.LIGHT_RAIN
|
|
339
299
|
elif value in (305, 356):
|
|
340
|
-
return
|
|
300
|
+
return cls.HEAVY_SHOWERS
|
|
341
301
|
elif value in (308, 359):
|
|
342
|
-
return
|
|
302
|
+
return cls.HEAVY_RAIN
|
|
343
303
|
elif value in (326, 368):
|
|
344
|
-
return
|
|
304
|
+
return cls.LIGHT_SNOW_SHOWERS
|
|
345
305
|
elif value in (371, 395):
|
|
346
|
-
return
|
|
306
|
+
return cls.HEAVY_SNOW_SHOWERS
|
|
347
307
|
|
|
348
308
|
@property
|
|
349
309
|
def emoji(self) -> str:
|
|
350
310
|
"""Emoji representation."""
|
|
351
311
|
|
|
352
|
-
|
|
353
|
-
return '☁️'
|
|
354
|
-
elif self is self.FOG:
|
|
355
|
-
return '🌫'
|
|
356
|
-
elif self is self.HEAVY_RAIN:
|
|
357
|
-
return '🌧'
|
|
358
|
-
elif self is self.HEAVY_SHOWERS:
|
|
359
|
-
return '🌧'
|
|
360
|
-
elif self is self.HEAVY_SNOW:
|
|
361
|
-
return '❄️'
|
|
362
|
-
elif self is self.HEAVY_SNOW_SHOWERS:
|
|
363
|
-
return '❄️'
|
|
364
|
-
elif self is self.LIGHT_RAIN:
|
|
365
|
-
return '🌦'
|
|
366
|
-
elif self is self.LIGHT_SHOWERS:
|
|
367
|
-
return '🌦'
|
|
368
|
-
elif self is self.LIGHT_SLEET:
|
|
369
|
-
return '🌧'
|
|
370
|
-
elif self is self.LIGHT_SLEET_SHOWERS:
|
|
371
|
-
return '🌧'
|
|
372
|
-
elif self is self.LIGHT_SNOW:
|
|
373
|
-
return '🌨'
|
|
374
|
-
elif self is self.LIGHT_SNOW_SHOWERS:
|
|
375
|
-
return '🌨'
|
|
376
|
-
elif self is self.PARTLY_CLOUDY:
|
|
377
|
-
return '⛅️'
|
|
378
|
-
elif self is self.SUNNY:
|
|
379
|
-
return '☀️'
|
|
380
|
-
elif self is self.THUNDERY_HEAVY_RAIN:
|
|
381
|
-
return '🌩'
|
|
382
|
-
elif self is self.THUNDERY_SHOWERS:
|
|
383
|
-
return '⛈'
|
|
384
|
-
elif self is self.THUNDERY_SNOW_SHOWERS:
|
|
385
|
-
return '⛈'
|
|
386
|
-
elif self is self.VERY_CLOUDY:
|
|
387
|
-
return '☁️'
|
|
388
|
-
else:
|
|
389
|
-
return '✨'
|
|
312
|
+
return KIND_EMOJIS[self._index]
|
|
390
313
|
|
|
391
314
|
|
|
392
315
|
class Phase(BasicEnum):
|
|
@@ -407,19 +330,4 @@ class Phase(BasicEnum):
|
|
|
407
330
|
def emoji(self) -> str:
|
|
408
331
|
"""Emoji representation."""
|
|
409
332
|
|
|
410
|
-
|
|
411
|
-
return '🌑'
|
|
412
|
-
elif self is self.WAXING_CRESCENT:
|
|
413
|
-
return '🌒'
|
|
414
|
-
elif self is self.FIRST_QUARTER:
|
|
415
|
-
return '🌓'
|
|
416
|
-
elif self is self.WAXING_GIBBOUS:
|
|
417
|
-
return '🌔'
|
|
418
|
-
elif self is self.FULL_MOON:
|
|
419
|
-
return '🌕'
|
|
420
|
-
elif self is self.WANING_GIBBOUS:
|
|
421
|
-
return '🌖'
|
|
422
|
-
elif self is self.LAST_QUARTER:
|
|
423
|
-
return '🌗'
|
|
424
|
-
else:
|
|
425
|
-
return '🌘'
|
|
333
|
+
return chr(0x1F311 + self._index)
|
python_weather/errors.py
CHANGED
|
@@ -1,28 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 typing import Optional
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: 2021-2026 null8626
|
|
26
3
|
|
|
27
4
|
|
|
28
5
|
class Error(Exception):
|
|
@@ -34,15 +11,19 @@ class Error(Exception):
|
|
|
34
11
|
class RequestError(Error):
|
|
35
12
|
"""Thrown upon HTTP request failure. Extends :class:`.Error`."""
|
|
36
13
|
|
|
37
|
-
__slots__: tuple[str, ...] =
|
|
14
|
+
__slots__: tuple[str, ...] = 'status', 'reason'
|
|
15
|
+
|
|
16
|
+
status: int | None
|
|
17
|
+
"""The status code."""
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
"""The
|
|
19
|
+
reason: str | None
|
|
20
|
+
"""The reason for this status code."""
|
|
41
21
|
|
|
42
|
-
def __init__(self, status:
|
|
22
|
+
def __init__(self, status: int | None, reason: str | None):
|
|
43
23
|
self.status = status
|
|
24
|
+
self.reason = reason
|
|
44
25
|
|
|
45
|
-
super().__init__()
|
|
26
|
+
super().__init__(f'{status}: {reason}')
|
|
46
27
|
|
|
47
|
-
def __repr__(self) -> str:
|
|
48
|
-
return f'<{__class__.__name__} status={self.status}>'
|
|
28
|
+
def __repr__(self) -> str: # pragma: nocover
|
|
29
|
+
return f'<{__class__.__module__}.{__class__.__name__} status={self.status} reason={self.reason!r}>'
|
python_weather/forecast.py
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
from collections.abc import
|
|
27
|
-
from typing import
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
28
7
|
|
|
29
|
-
from .enums import Phase,
|
|
30
|
-
from .constants import
|
|
8
|
+
from .enums import Phase, HeatIndex
|
|
9
|
+
from .constants import LATLON_REGEX
|
|
31
10
|
from .base import BaseForecast
|
|
32
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .constants import _Unit
|
|
14
|
+
from .enums import Locale
|
|
15
|
+
|
|
33
16
|
|
|
34
17
|
class HourlyForecast(BaseForecast):
|
|
35
18
|
"""A weather forecast for a specific hour."""
|
|
@@ -45,7 +28,6 @@ class HourlyForecast(BaseForecast):
|
|
|
45
28
|
'chances_of_sunshine',
|
|
46
29
|
'chances_of_thunder',
|
|
47
30
|
'chances_of_windy',
|
|
48
|
-
'cloud_cover',
|
|
49
31
|
'time',
|
|
50
32
|
'dew_point',
|
|
51
33
|
'heat_index',
|
|
@@ -83,9 +65,6 @@ class HourlyForecast(BaseForecast):
|
|
|
83
65
|
chances_of_windy: int
|
|
84
66
|
"""Chances of windy in percent."""
|
|
85
67
|
|
|
86
|
-
cloud_cover: int
|
|
87
|
-
"""The cloud cover value in percent."""
|
|
88
|
-
|
|
89
68
|
time: 'time'
|
|
90
69
|
"""The local time in hours and minutes."""
|
|
91
70
|
|
|
@@ -101,7 +80,7 @@ class HourlyForecast(BaseForecast):
|
|
|
101
80
|
wind_gust: int
|
|
102
81
|
"""The wind gust value in either kilometers/hour or miles/hour."""
|
|
103
82
|
|
|
104
|
-
def __init__(self, json: dict, unit: _Unit, locale: Locale):
|
|
83
|
+
def __init__(self, json: dict, unit: '_Unit', locale: 'Locale'):
|
|
105
84
|
# For inheritance purposes.
|
|
106
85
|
if 'temp_C' not in json:
|
|
107
86
|
json['temp_C'] = json.pop('tempC')
|
|
@@ -122,7 +101,6 @@ class HourlyForecast(BaseForecast):
|
|
|
122
101
|
self.chances_of_sunshine = int(json['chanceofsunshine'])
|
|
123
102
|
self.chances_of_thunder = int(json['chanceofthunder'])
|
|
124
103
|
self.chances_of_windy = int(json['chanceofwindy'])
|
|
125
|
-
self.cloud_cover = int(json['cloudcover'])
|
|
126
104
|
self.time = time() if len(t) < 3 else datetime.strptime(t, '%H%M').time()
|
|
127
105
|
self.dew_point = int(json[f'DewPoint{unit.temperature}'])
|
|
128
106
|
self.heat_index = HeatIndex._new(
|
|
@@ -135,7 +113,7 @@ class HourlyForecast(BaseForecast):
|
|
|
135
113
|
super().__init__(json, unit, locale)
|
|
136
114
|
|
|
137
115
|
def __repr__(self) -> str:
|
|
138
|
-
return f'<{__class__.__name__} time={self.time!r} temperature={self.temperature}
|
|
116
|
+
return f'<{__class__.__module__}.{__class__.__name__} time={self.time!r} temperature={self.temperature} kind={self.kind!r}>'
|
|
139
117
|
|
|
140
118
|
|
|
141
119
|
class DailyForecast:
|
|
@@ -163,16 +141,16 @@ class DailyForecast:
|
|
|
163
141
|
moon_phase: Phase
|
|
164
142
|
"""The moon's phase."""
|
|
165
143
|
|
|
166
|
-
moonrise:
|
|
144
|
+
moonrise: time | None
|
|
167
145
|
"""The local time when the moon rises."""
|
|
168
146
|
|
|
169
|
-
moonset:
|
|
147
|
+
moonset: time | None
|
|
170
148
|
"""The local time when the moon sets."""
|
|
171
149
|
|
|
172
|
-
sunrise:
|
|
150
|
+
sunrise: time | None
|
|
173
151
|
"""The local time when the sun rises."""
|
|
174
152
|
|
|
175
|
-
sunset:
|
|
153
|
+
sunset: time | None
|
|
176
154
|
"""The local time when the sun sets."""
|
|
177
155
|
|
|
178
156
|
date: 'date'
|
|
@@ -196,7 +174,7 @@ class DailyForecast:
|
|
|
196
174
|
hourly_forecasts: list[HourlyForecast]
|
|
197
175
|
"""The hourly forecasts of this day."""
|
|
198
176
|
|
|
199
|
-
def __init__(self, json: dict, unit: _Unit, locale: Locale):
|
|
177
|
+
def __init__(self, json: dict, unit: '_Unit', locale: 'Locale'):
|
|
200
178
|
astronomy = json['astronomy'][0]
|
|
201
179
|
|
|
202
180
|
self.moon_illumination = int(astronomy['moon_illumination'])
|
|
@@ -216,19 +194,19 @@ class DailyForecast:
|
|
|
216
194
|
]
|
|
217
195
|
|
|
218
196
|
@staticmethod
|
|
219
|
-
def __parse_time(timestamp: str) ->
|
|
197
|
+
def __parse_time(timestamp: str) -> time | None:
|
|
220
198
|
try:
|
|
221
199
|
return datetime.strptime(timestamp, '%I:%M %p').time()
|
|
222
|
-
except ValueError:
|
|
200
|
+
except ValueError: # pragma: nocover
|
|
223
201
|
...
|
|
224
202
|
|
|
225
203
|
def __repr__(self) -> str:
|
|
226
|
-
return f'<{__class__.__name__} date={self.date!r} temperature={self.temperature}>'
|
|
204
|
+
return f'<{__class__.__module__}.{__class__.__name__} date={self.date!r} temperature={self.temperature}>'
|
|
227
205
|
|
|
228
206
|
def __len__(self) -> int:
|
|
229
207
|
return len(self.hourly_forecasts)
|
|
230
208
|
|
|
231
|
-
def __iter__(self) ->
|
|
209
|
+
def __iter__(self) -> Iterator[HourlyForecast]:
|
|
232
210
|
return iter(self.hourly_forecasts)
|
|
233
211
|
|
|
234
212
|
|
|
@@ -266,7 +244,7 @@ class Forecast(BaseForecast):
|
|
|
266
244
|
daily_forecasts: list[DailyForecast]
|
|
267
245
|
"""Daily weather forecasts in this location."""
|
|
268
246
|
|
|
269
|
-
def __init__(self, json: dict, unit: _Unit, locale: Locale):
|
|
247
|
+
def __init__(self, json: dict, unit: '_Unit', locale: 'Locale'):
|
|
270
248
|
current = json['current_condition'][0]
|
|
271
249
|
nearest = json['nearest_area'][0]
|
|
272
250
|
|
|
@@ -280,9 +258,12 @@ class Forecast(BaseForecast):
|
|
|
280
258
|
req = next(filter(lambda x: x['type'] == 'LatLon', json['request']))
|
|
281
259
|
match = LATLON_REGEX.match(req['query'])
|
|
282
260
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
261
|
+
if TYPE_CHECKING:
|
|
262
|
+
assert match is not None
|
|
263
|
+
|
|
264
|
+
self.coordinates = float(match[1]), float(match[2])
|
|
265
|
+
except (AssertionError, KeyError, StopIteration):
|
|
266
|
+
self.coordinates = float(nearest['latitude']), float(nearest['longitude'])
|
|
286
267
|
|
|
287
268
|
self.daily_forecasts = [
|
|
288
269
|
DailyForecast(elem, unit, locale) for elem in json['weather']
|
|
@@ -291,10 +272,10 @@ class Forecast(BaseForecast):
|
|
|
291
272
|
super().__init__(current, unit, locale)
|
|
292
273
|
|
|
293
274
|
def __repr__(self) -> str:
|
|
294
|
-
return f'<{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature}>'
|
|
275
|
+
return f'<{__class__.__module__}.{__class__.__name__} location={self.location!r} datetime={self.datetime!r} temperature={self.temperature}>'
|
|
295
276
|
|
|
296
277
|
def __len__(self) -> int:
|
|
297
278
|
return len(self.daily_forecasts)
|
|
298
279
|
|
|
299
|
-
def __iter__(self) ->
|
|
280
|
+
def __iter__(self) -> Iterator[DailyForecast]:
|
|
300
281
|
return iter(self.daily_forecasts)
|
python_weather/version.py
CHANGED
|
@@ -1,110 +1,104 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-weather
|
|
3
|
-
Version: 2.1.
|
|
4
|
-
Summary: A free and asynchronous weather API wrapper made in Python, for Python.
|
|
5
|
-
Author: null8626
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Documentation, https://python-weather.readthedocs.io/en/latest/
|
|
8
|
-
Project-URL: Repository, https://github.com/null8626/python-weather
|
|
9
|
-
Project-URL: Changelog, https://python-weather.readthedocs.io/en/latest/changelog.html
|
|
10
|
-
Project-URL: Donate via GitHub Sponsors, https://github.com/sponsors/null8626
|
|
11
|
-
Project-URL: Donate via Ko-fi, https://ko-fi.com/null8626
|
|
12
|
-
Keywords: weather,forecast,weather-
|
|
13
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
-
Classifier: Intended Audience :: Education
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: Intended Audience :: Information Technology
|
|
17
|
-
Classifier: Natural Language :: English
|
|
18
|
-
Classifier: Operating System :: OS Independent
|
|
19
|
-
Classifier: Topic :: Education
|
|
20
|
-
Classifier: Topic :: Internet
|
|
21
|
-
Classifier: Topic :: Scientific/Engineering
|
|
22
|
-
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
23
|
-
Classifier: Topic :: Software Development
|
|
24
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
-
Classifier:
|
|
26
|
-
Classifier: Programming Language :: Python
|
|
27
|
-
Classifier: Programming Language :: Python :: 3
|
|
28
|
-
Classifier: Programming Language :: Python :: 3
|
|
29
|
-
Classifier: Programming Language :: Python :: 3.
|
|
30
|
-
Classifier: Programming Language :: Python :: 3.
|
|
31
|
-
Classifier: Programming Language :: Python :: 3.
|
|
32
|
-
Classifier: Programming Language :: Python :: 3.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
[pypi-image]: https://img.shields.io/pypi/v/python-weather.svg?style=flat-square
|
|
43
|
-
[pypi-
|
|
44
|
-
[
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
[
|
|
48
|
-
[ko-fi-
|
|
49
|
-
[ko-fi-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
import
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
async def main() -> None:
|
|
74
|
-
|
|
75
|
-
# Declare the client. The measuring unit used defaults to the metric system (celcius, km/h, etc.)
|
|
76
|
-
async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
|
|
77
|
-
|
|
78
|
-
# Fetch a weather forecast from a city.
|
|
79
|
-
weather = await client.get('New York')
|
|
80
|
-
|
|
81
|
-
# Fetch the temperature for today.
|
|
82
|
-
print(weather.temperature)
|
|
83
|
-
|
|
84
|
-
# Fetch weather forecast for upcoming days.
|
|
85
|
-
for daily in weather:
|
|
86
|
-
print(daily)
|
|
87
|
-
|
|
88
|
-
# Each daily forecast has their own hourly forecasts.
|
|
89
|
-
for hourly in daily:
|
|
90
|
-
print(f' --> {hourly!r}')
|
|
91
|
-
|
|
92
|
-
if __name__ == '__main__':
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
## Donations
|
|
107
|
-
|
|
108
|
-
If you want to support this project, consider donating! ❤
|
|
109
|
-
|
|
110
|
-
[![ko-fi][ko-fi-image]][ko-fi-url]
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-weather
|
|
3
|
+
Version: 2.1.2
|
|
4
|
+
Summary: A free and asynchronous weather API wrapper made in Python, for Python.
|
|
5
|
+
Author: null8626
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://python-weather.readthedocs.io/en/latest/
|
|
8
|
+
Project-URL: Repository, https://github.com/null8626/python-weather
|
|
9
|
+
Project-URL: Changelog, https://python-weather.readthedocs.io/en/latest/changelog.html
|
|
10
|
+
Project-URL: Donate via GitHub Sponsors, https://github.com/sponsors/null8626
|
|
11
|
+
Project-URL: Donate via Ko-fi, https://ko-fi.com/null8626
|
|
12
|
+
Keywords: weather,forecast,weather-forecast
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Education
|
|
20
|
+
Classifier: Topic :: Internet
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
23
|
+
Classifier: Topic :: Software Development
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Programming Language :: Python
|
|
26
|
+
Classifier: Programming Language :: Python :: 3
|
|
27
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: aiohttp>=3.13.3
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
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]
|
|
40
|
+
|
|
41
|
+
[pypi-url]: https://pypi.org/project/python-weather/
|
|
42
|
+
[pypi-image]: https://img.shields.io/pypi/v/python-weather.svg?style=flat-square
|
|
43
|
+
[pypi-downloads-image]: https://img.shields.io/pypi/dm/python-weather?style=flat-square
|
|
44
|
+
[codacy-url]: https://app.codacy.com/gh/null8626/python-weather/dashboard
|
|
45
|
+
[codacy-image]: https://app.codacy.com/project/badge/Grade/0f7721b7e4314a748c75a04f0a7e0ce3
|
|
46
|
+
[codecov-url]: https://codecov.io/gh/null8626/python-weather
|
|
47
|
+
[codecov-image]: https://codecov.io/gh/null8626/python-weather/graph/badge.svg
|
|
48
|
+
[ko-fi-url]: https://ko-fi.com/null8626
|
|
49
|
+
[ko-fi-image]: https://ko-fi.com/img/githubbutton_sm.svg
|
|
50
|
+
[ko-fi-brief-image]: https://img.shields.io/badge/donations-ko--fi-red?color=ff5e5b&style=flat-square
|
|
51
|
+
|
|
52
|
+
A free and asynchronous weather Python API wrapper made in Python, for Python.
|
|
53
|
+
|
|
54
|
+
## Getting started
|
|
55
|
+
|
|
56
|
+
Run the following command in your terminal:
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
pip install python-weather
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Example
|
|
63
|
+
|
|
64
|
+
For more information, please read the [documentation](https://python-weather.readthedocs.io/en/latest/).
|
|
65
|
+
|
|
66
|
+
```py
|
|
67
|
+
# Import the module.
|
|
68
|
+
import python_weather
|
|
69
|
+
|
|
70
|
+
import asyncio
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def main() -> None:
|
|
74
|
+
|
|
75
|
+
# Declare the client. The measuring unit used defaults to the metric system (celcius, km/h, etc.)
|
|
76
|
+
async with python_weather.Client(unit=python_weather.IMPERIAL) as client:
|
|
77
|
+
|
|
78
|
+
# Fetch a weather forecast from a city.
|
|
79
|
+
weather = await client.get('New York')
|
|
80
|
+
|
|
81
|
+
# Fetch the temperature for today.
|
|
82
|
+
print(weather.temperature)
|
|
83
|
+
|
|
84
|
+
# Fetch weather forecast for upcoming days.
|
|
85
|
+
for daily in weather:
|
|
86
|
+
print(daily)
|
|
87
|
+
|
|
88
|
+
# Each daily forecast has their own hourly forecasts.
|
|
89
|
+
for hourly in daily:
|
|
90
|
+
print(f' --> {hourly!r}')
|
|
91
|
+
|
|
92
|
+
if __name__ == '__main__':
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Data source
|
|
97
|
+
|
|
98
|
+
This library depends on [`wttr.in`](https://github.com/chubin/wttr.in), which uses data from the [World Weather Online API](https://www.worldweatheronline.com/weather-api/).
|
|
99
|
+
|
|
100
|
+
## Donations
|
|
101
|
+
|
|
102
|
+
If you want to support this project, consider donating! ❤
|
|
103
|
+
|
|
104
|
+
[![ko-fi][ko-fi-image]][ko-fi-url]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
python_weather/__init__.py,sha256=2Slw4Eyvlg9kGcXsaklJwigp6dIKEgmlsM1VH__qNaw,744
|
|
2
|
+
python_weather/base.py,sha256=5zoReBO7OCiPyI7KzIvJ_Z6Xc245UKjcauSB1frQ_L8,3454
|
|
3
|
+
python_weather/client.py,sha256=myVb2oKoPwVh3TZz-F1Vwez1_xY6z3ii7PfPIxVWtlQ,4845
|
|
4
|
+
python_weather/constants.py,sha256=Dv91U3EbXI6hHuhAtsns6NGNZcDWxMJm1OWu3ETIV-g,1289
|
|
5
|
+
python_weather/enums.py,sha256=pCHiX5OpAjqLCreoF9xUpHtGyDCEXI1bOyP5quolAeY,7349
|
|
6
|
+
python_weather/errors.py,sha256=o12OWgnCx7pts0XtxhSOPMonYKmSS0q78r9eeus9eNM,766
|
|
7
|
+
python_weather/forecast.py,sha256=ECYDRsVro0OtxrJ_1gg0OUbvTg6r2ky7EdVF6xQ01wM,8099
|
|
8
|
+
python_weather/version.py,sha256=J8dprZh03pwDdgwroPTSQxyCxe6YQe772K1jAXvo0Uo,95
|
|
9
|
+
python_weather-2.1.2.dist-info/licenses/LICENSE,sha256=SDMQ_fK2dwWVC-3qqONf6VVMnBe-GtBtlfYZIN2SBXw,1080
|
|
10
|
+
python_weather-2.1.2.dist-info/METADATA,sha256=op6Cb4zixM7vrWZlLdPGn6COQiMmNGFA3JpsB6Cg6aM,3967
|
|
11
|
+
python_weather-2.1.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
12
|
+
python_weather-2.1.2.dist-info/top_level.txt,sha256=NDqLSO3dHB1tjjiNJfEg-a9_eoOmawAZynZsy9Yu48w,15
|
|
13
|
+
python_weather-2.1.2.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
python_weather/__init__.py,sha256=yjH5DD6J3qFo0EoFymNPI-fZy7va8aWhQAn0UQnLLnk,1629
|
|
2
|
-
python_weather/base.py,sha256=4NcOCR2_4kdR5bJhTvTfDF_-rY7a1MJEvAumdK56N18,4312
|
|
3
|
-
python_weather/client.py,sha256=mLALGZ-1z_rQPxoy9fFu4ctXOenCHBKD0QD32BFOUq0,5419
|
|
4
|
-
python_weather/constants.py,sha256=KRoi7NcHOLRByGaSLAvRWE_XC7pce1ERQYQABvnOsBc,2008
|
|
5
|
-
python_weather/enums.py,sha256=QUMqLAC6jkiO6SOSPoZU9rj4rgSxxEUa4TxJhQ0lcK4,10520
|
|
6
|
-
python_weather/errors.py,sha256=g466BDA7YvrLb7g0JIkd8f1s_04oj9SwYzYL5o3FB9o,1634
|
|
7
|
-
python_weather/forecast.py,sha256=G3xlKbdi77OcfMgEO2qMmKl3bpYHucClDz6h6UD_nVM,9061
|
|
8
|
-
python_weather/version.py,sha256=n--7vRgXQjzasMQnq-ucPzeub4H3X8UwSt0klWLlzqk,18
|
|
9
|
-
python_weather-2.1.0.dist-info/licenses/LICENSE,sha256=bJLXQOjz80Su3DTNc67WbjYJUQJkZAF2KsSVE11fInk,1080
|
|
10
|
-
python_weather-2.1.0.dist-info/METADATA,sha256=_pvvi_q4Zi3F1rXmjXeazhf7e7nhvmkKjAH8BILetzE,4174
|
|
11
|
-
python_weather-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
python_weather-2.1.0.dist-info/top_level.txt,sha256=NDqLSO3dHB1tjjiNJfEg-a9_eoOmawAZynZsy9Yu48w,15
|
|
13
|
-
python_weather-2.1.0.dist-info/RECORD,,
|
|
File without changes
|