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