python-google-weather-api 0.0.10__tar.gz → 0.0.11__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_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/PKG-INFO +1 -1
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/pyproject.toml +1 -1
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/api.py +18 -5
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/model.py +156 -35
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/python_google_weather_api.egg-info/PKG-INFO +1 -1
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/tests/test_api.py +28 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/tests/test_model.py +102 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/LICENSE +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/README.md +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/setup.cfg +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/__init__.py +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/exceptions.py +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/py.typed +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/python_google_weather_api.egg-info/SOURCES.txt +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/python_google_weather_api.egg-info/dependency_links.txt +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/python_google_weather_api.egg-info/requires.txt +0 -0
- {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/python_google_weather_api.egg-info/top_level.txt +0 -0
{python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/api.py
RENAMED
|
@@ -4,9 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from http import HTTPStatus
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
|
+
from mashumaro.exceptions import InvalidFieldValue, MissingField
|
|
10
11
|
|
|
11
12
|
from .exceptions import (
|
|
12
13
|
GoogleWeatherApiAuthError,
|
|
@@ -15,6 +16,7 @@ from .exceptions import (
|
|
|
15
16
|
GoogleWeatherApiResponseError,
|
|
16
17
|
)
|
|
17
18
|
from .model import (
|
|
19
|
+
BaseModel,
|
|
18
20
|
CurrentConditionsResponse,
|
|
19
21
|
DailyForecastResponse,
|
|
20
22
|
HourlyForecastResponse,
|
|
@@ -23,6 +25,17 @@ from .model import (
|
|
|
23
25
|
|
|
24
26
|
_LOGGER = logging.getLogger(__name__)
|
|
25
27
|
|
|
28
|
+
_ResponseT = TypeVar("_ResponseT", bound=BaseModel)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_response(model: type[_ResponseT], data: dict[str, Any]) -> _ResponseT:
|
|
32
|
+
"""Deserialize an API response dictionary, wrapping schema errors."""
|
|
33
|
+
try:
|
|
34
|
+
return model.from_dict(data)
|
|
35
|
+
except (InvalidFieldValue, MissingField) as err:
|
|
36
|
+
raise GoogleWeatherApiResponseError(str(err)) from err
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
_BASE_URL = "https://weather.googleapis.com/v1"
|
|
27
40
|
# See https://docs.cloud.google.com/docs/authentication/api-keys-use
|
|
28
41
|
_API_KEY_HEADER = "X-Goog-Api-Key"
|
|
@@ -140,7 +153,7 @@ class GoogleWeatherApi:
|
|
|
140
153
|
"location.longitude": longitude,
|
|
141
154
|
},
|
|
142
155
|
)
|
|
143
|
-
return CurrentConditionsResponse
|
|
156
|
+
return _parse_response(CurrentConditionsResponse, data)
|
|
144
157
|
|
|
145
158
|
async def async_get_hourly_forecast(self, latitude: float, longitude: float, hours: int = 48) -> HourlyForecastResponse:
|
|
146
159
|
"""Fetch hourly weather forecast.
|
|
@@ -157,7 +170,7 @@ class GoogleWeatherApi:
|
|
|
157
170
|
"forecastHours",
|
|
158
171
|
hours,
|
|
159
172
|
)
|
|
160
|
-
return HourlyForecastResponse
|
|
173
|
+
return _parse_response(HourlyForecastResponse, data)
|
|
161
174
|
|
|
162
175
|
async def async_get_daily_forecast(self, latitude: float, longitude: float, days: int = 10) -> DailyForecastResponse:
|
|
163
176
|
"""Fetch daily weather forecast.
|
|
@@ -174,7 +187,7 @@ class GoogleWeatherApi:
|
|
|
174
187
|
"forecastDays",
|
|
175
188
|
days,
|
|
176
189
|
)
|
|
177
|
-
return DailyForecastResponse
|
|
190
|
+
return _parse_response(DailyForecastResponse, data)
|
|
178
191
|
|
|
179
192
|
async def async_get_minute_forecast(
|
|
180
193
|
self, latitude: float, longitude: float, page_size: int = 360
|
|
@@ -197,4 +210,4 @@ class GoogleWeatherApi:
|
|
|
197
210
|
page_size,
|
|
198
211
|
include_language_code=False,
|
|
199
212
|
)
|
|
200
|
-
return MinuteForecastResponse
|
|
213
|
+
return _parse_response(MinuteForecastResponse, data)
|
|
@@ -93,14 +93,14 @@ class Temperature(BaseModel):
|
|
|
93
93
|
class TemperatureUnit(ApiStrEnum):
|
|
94
94
|
"""Represents a unit used to measure temperatures."""
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
UNIT_UNSPECIFIED = "UNIT_UNSPECIFIED"
|
|
97
97
|
CELSIUS = "CELSIUS"
|
|
98
98
|
FAHRENHEIT = "FAHRENHEIT"
|
|
99
99
|
|
|
100
|
-
degrees: float
|
|
100
|
+
degrees: float | None = None
|
|
101
101
|
"""The temperature value (in degrees) in the specified unit."""
|
|
102
102
|
|
|
103
|
-
unit: TemperatureUnit
|
|
103
|
+
unit: TemperatureUnit = TemperatureUnit.UNIT_UNSPECIFIED
|
|
104
104
|
"""The code for the unit used to measure the temperature value."""
|
|
105
105
|
|
|
106
106
|
|
|
@@ -115,10 +115,10 @@ class QuantitativePrecipitationForecast(BaseModel):
|
|
|
115
115
|
MILLIMETERS = "MILLIMETERS"
|
|
116
116
|
INCHES = "INCHES"
|
|
117
117
|
|
|
118
|
-
quantity: float
|
|
118
|
+
quantity: float | None = None
|
|
119
119
|
"""The amount of precipitation, measured as liquid water equivalent."""
|
|
120
120
|
|
|
121
|
-
unit: Unit
|
|
121
|
+
unit: Unit = Unit.UNIT_UNSPECIFIED
|
|
122
122
|
"""The code of the unit used to measure the amount of accumulated precipitation."""
|
|
123
123
|
|
|
124
124
|
|
|
@@ -129,7 +129,7 @@ class PrecipitationProbability(BaseModel):
|
|
|
129
129
|
class PrecipitationType(ApiStrEnum):
|
|
130
130
|
"""Represents the type of precipitation at a given location."""
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
TYPE_UNSPECIFIED = "TYPE_UNSPECIFIED"
|
|
133
133
|
NONE = "NONE"
|
|
134
134
|
SNOW = "SNOW"
|
|
135
135
|
RAIN = "RAIN"
|
|
@@ -138,6 +138,7 @@ class PrecipitationProbability(BaseModel):
|
|
|
138
138
|
RAIN_AND_SNOW = "RAIN_AND_SNOW"
|
|
139
139
|
SLEET = "SLEET"
|
|
140
140
|
FREEZING_RAIN = "FREEZING_RAIN"
|
|
141
|
+
PRECIPITATION_TYPE_HAIL = "PRECIPITATION_TYPE_HAIL"
|
|
141
142
|
|
|
142
143
|
type: PrecipitationType
|
|
143
144
|
"""A code that indicates the type of precipitation."""
|
|
@@ -167,14 +168,14 @@ class WindSpeed(BaseModel):
|
|
|
167
168
|
class SpeedUnit(ApiStrEnum):
|
|
168
169
|
"""Represents the unit used to measure speed."""
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
UNIT_UNSPECIFIED = "UNIT_UNSPECIFIED"
|
|
171
172
|
KILOMETERS_PER_HOUR = "KILOMETERS_PER_HOUR"
|
|
172
173
|
MILES_PER_HOUR = "MILES_PER_HOUR"
|
|
173
174
|
|
|
174
|
-
value: float
|
|
175
|
+
value: float | None = None
|
|
175
176
|
"""The value of the wind speed."""
|
|
176
177
|
|
|
177
|
-
unit: SpeedUnit
|
|
178
|
+
unit: SpeedUnit = SpeedUnit.UNIT_UNSPECIFIED
|
|
178
179
|
"""The code that represents the unit used to measure the wind speed."""
|
|
179
180
|
|
|
180
181
|
|
|
@@ -185,7 +186,7 @@ class WindDirection(BaseModel):
|
|
|
185
186
|
class CardinalDirection(ApiStrEnum):
|
|
186
187
|
"""Represents a cardinal direction (including ordinal directions)."""
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
CARDINAL_UNSPECIFIED = "CARDINAL_UNSPECIFIED"
|
|
189
190
|
NORTH = "NORTH"
|
|
190
191
|
NORTH_NORTHEAST = "NORTH_NORTHEAST"
|
|
191
192
|
NORTHEAST = "NORTHEAST"
|
|
@@ -220,7 +221,7 @@ class Wind(BaseModel):
|
|
|
220
221
|
speed: WindSpeed
|
|
221
222
|
"""The speed of the wind."""
|
|
222
223
|
|
|
223
|
-
gust: WindSpeed
|
|
224
|
+
gust: WindSpeed | None = None
|
|
224
225
|
"""The wind gust (sudden increase in the wind speed)."""
|
|
225
226
|
|
|
226
227
|
|
|
@@ -235,10 +236,10 @@ class Visibility(BaseModel):
|
|
|
235
236
|
KILOMETERS = "KILOMETERS"
|
|
236
237
|
MILES = "MILES"
|
|
237
238
|
|
|
238
|
-
distance: float
|
|
239
|
+
distance: float | None = None
|
|
239
240
|
"""The visibility distance in the specified unit."""
|
|
240
241
|
|
|
241
|
-
unit: Unit
|
|
242
|
+
unit: Unit = Unit.UNIT_UNSPECIFIED
|
|
242
243
|
"""The code that represents the unit used to measure the distance."""
|
|
243
244
|
|
|
244
245
|
|
|
@@ -290,6 +291,120 @@ class WeatherCondition(BaseModel):
|
|
|
290
291
|
LIGHT_THUNDERSTORM_RAIN = "LIGHT_THUNDERSTORM_RAIN"
|
|
291
292
|
SCATTERED_THUNDERSTORMS = "SCATTERED_THUNDERSTORMS"
|
|
292
293
|
HEAVY_THUNDERSTORM = "HEAVY_THUNDERSTORM"
|
|
294
|
+
CLEAR_AFTER_FOG_IN_THE_MORNING = "CLEAR_AFTER_FOG_IN_THE_MORNING"
|
|
295
|
+
CLEAR_BUT_LATER_TURNING_CLOUDY = "CLEAR_BUT_LATER_TURNING_CLOUDY"
|
|
296
|
+
CLEAR_BUT_LATER_TURNING_TO_RAIN_AND_SNOW_MAIN_CONSTITUENT_RAIN = (
|
|
297
|
+
"CLEAR_BUT_LATER_TURNING_TO_RAIN_AND_SNOW_MAIN_CONSTITUENT_RAIN"
|
|
298
|
+
)
|
|
299
|
+
CLEAR_BUT_LATER_TURNING_TO_SNOW_AND_RAIN_MAIN_CONSTITUENT_SNOW = (
|
|
300
|
+
"CLEAR_BUT_LATER_TURNING_TO_SNOW_AND_RAIN_MAIN_CONSTITUENT_SNOW"
|
|
301
|
+
)
|
|
302
|
+
CLEAR_BUT_THUNDERSTORMS_LATER = "CLEAR_BUT_THUNDERSTORMS_LATER"
|
|
303
|
+
CLEAR_IN_THE_MORNING_BUT_CLOUDY_IN_THE_EVENING = "CLEAR_IN_THE_MORNING_BUT_CLOUDY_IN_THE_EVENING"
|
|
304
|
+
CLEAR_IN_THE_MORNING_WITH_BRIEF_RAIN_IN_THE_EVENING = "CLEAR_IN_THE_MORNING_WITH_BRIEF_RAIN_IN_THE_EVENING"
|
|
305
|
+
CLEAR_MORNING_WITH_BRIEF_RAIN = "CLEAR_MORNING_WITH_BRIEF_RAIN"
|
|
306
|
+
CLEAR_WITH_BRIEF_RAIN = "CLEAR_WITH_BRIEF_RAIN"
|
|
307
|
+
CLEAR_WITH_BRIEF_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT = "CLEAR_WITH_BRIEF_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT"
|
|
308
|
+
CLEAR_WITH_BRIEF_RAIN_LATER = "CLEAR_WITH_BRIEF_RAIN_LATER"
|
|
309
|
+
CLEAR_WITH_BRIEF_SNOW = "CLEAR_WITH_BRIEF_SNOW"
|
|
310
|
+
CLEAR_WITH_BRIEF_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT = "CLEAR_WITH_BRIEF_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT"
|
|
311
|
+
CLEAR_WITH_BRIEF_SNOW_LATER = "CLEAR_WITH_BRIEF_SNOW_LATER"
|
|
312
|
+
CLEAR_WITH_PERIODIC_CLOUDS_LATER = "CLEAR_WITH_PERIODIC_CLOUDS_LATER"
|
|
313
|
+
CLEAR_WITH_PERIODIC_RAIN = "CLEAR_WITH_PERIODIC_RAIN"
|
|
314
|
+
CLEAR_WITH_PERIODIC_RAIN_ACCOMPANIED_BY_THUNDER = "CLEAR_WITH_PERIODIC_RAIN_ACCOMPANIED_BY_THUNDER"
|
|
315
|
+
CLEAR_WITH_PERIODIC_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT = (
|
|
316
|
+
"CLEAR_WITH_PERIODIC_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT"
|
|
317
|
+
)
|
|
318
|
+
CLEAR_WITH_PERIODIC_RAIN_LATER = "CLEAR_WITH_PERIODIC_RAIN_LATER"
|
|
319
|
+
CLEAR_WITH_PERIODIC_SNOW = "CLEAR_WITH_PERIODIC_SNOW"
|
|
320
|
+
CLEAR_WITH_PERIODIC_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT = (
|
|
321
|
+
"CLEAR_WITH_PERIODIC_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT"
|
|
322
|
+
)
|
|
323
|
+
CLEAR_WITH_PERIODIC_SNOW_LATER = "CLEAR_WITH_PERIODIC_SNOW_LATER"
|
|
324
|
+
CLEAR_WITH_RAIN_AFTER_MIDNIGHT = "CLEAR_WITH_RAIN_AFTER_MIDNIGHT"
|
|
325
|
+
CLEAR_WITH_RAIN_IN_THE_NIGHT = "CLEAR_WITH_RAIN_IN_THE_NIGHT"
|
|
326
|
+
CLEAR_WITH_RAIN_LATER = "CLEAR_WITH_RAIN_LATER"
|
|
327
|
+
CLEAR_WITH_SNOW_ALONG_THE_MOUNTAINS = "CLEAR_WITH_SNOW_ALONG_THE_MOUNTAINS"
|
|
328
|
+
CLEAR_WITH_SNOW_LATER = "CLEAR_WITH_SNOW_LATER"
|
|
329
|
+
CLEAR_WITH_THUNDERSTORMS_ALONG_THE_MOUNTAINS = "CLEAR_WITH_THUNDERSTORMS_ALONG_THE_MOUNTAINS"
|
|
330
|
+
CLEAR_WITH_THUNDERSTORMS_IN_THE_AFTERNOON = "CLEAR_WITH_THUNDERSTORMS_IN_THE_AFTERNOON"
|
|
331
|
+
CLOUDY_BUT_CLEAR_LATER = "CLOUDY_BUT_CLEAR_LATER"
|
|
332
|
+
CLOUDY_BUT_LATER_TURNING_TO_RAIN_AND_SNOW_MAIN_CONSTITUENT_RAIN = (
|
|
333
|
+
"CLOUDY_BUT_LATER_TURNING_TO_RAIN_AND_SNOW_MAIN_CONSTITUENT_RAIN"
|
|
334
|
+
)
|
|
335
|
+
CLOUDY_BUT_LATER_TURNING_TO_SNOW_AND_RAIN_MAIN_CONSTITUENT_SNOW = (
|
|
336
|
+
"CLOUDY_BUT_LATER_TURNING_TO_SNOW_AND_RAIN_MAIN_CONSTITUENT_SNOW"
|
|
337
|
+
)
|
|
338
|
+
CLOUDY_BUT_PERIODICALLY_CLEAR_LATER = "CLOUDY_BUT_PERIODICALLY_CLEAR_LATER"
|
|
339
|
+
CLOUDY_BUT_THUNDERSTORMS_LATER = "CLOUDY_BUT_THUNDERSTORMS_LATER"
|
|
340
|
+
CLOUDY_MORNING_WITH_BRIEF_RAIN = "CLOUDY_MORNING_WITH_BRIEF_RAIN"
|
|
341
|
+
CLOUDY_PERIODICALLY_CLEAR = "CLOUDY_PERIODICALLY_CLEAR"
|
|
342
|
+
CLOUDY_SNOWING_IN_THE_EVENING = "CLOUDY_SNOWING_IN_THE_EVENING"
|
|
343
|
+
CLOUDY_SNOWING_IN_THE_NIGHT = "CLOUDY_SNOWING_IN_THE_NIGHT"
|
|
344
|
+
CLOUDY_WITH_BRIEF_RAIN = "CLOUDY_WITH_BRIEF_RAIN"
|
|
345
|
+
CLOUDY_WITH_BRIEF_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT = "CLOUDY_WITH_BRIEF_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT"
|
|
346
|
+
CLOUDY_WITH_BRIEF_RAIN_LATER = "CLOUDY_WITH_BRIEF_RAIN_LATER"
|
|
347
|
+
CLOUDY_WITH_BRIEF_SNOW = "CLOUDY_WITH_BRIEF_SNOW"
|
|
348
|
+
CLOUDY_WITH_BRIEF_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT = "CLOUDY_WITH_BRIEF_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT"
|
|
349
|
+
CLOUDY_WITH_BRIEF_SNOW_LATER = "CLOUDY_WITH_BRIEF_SNOW_LATER"
|
|
350
|
+
CLOUDY_WITH_BRIEF_THUNDERSTORM = "CLOUDY_WITH_BRIEF_THUNDERSTORM"
|
|
351
|
+
CLOUDY_WITH_FOG_ON_THE_OCEAN_AND_AT_THE_SEASHORE = "CLOUDY_WITH_FOG_ON_THE_OCEAN_AND_AT_THE_SEASHORE"
|
|
352
|
+
CLOUDY_WITH_PERIODIC_RAIN = "CLOUDY_WITH_PERIODIC_RAIN"
|
|
353
|
+
CLOUDY_WITH_PERIODIC_RAIN_ACCOMPANIED_BY_THUNDER = "CLOUDY_WITH_PERIODIC_RAIN_ACCOMPANIED_BY_THUNDER"
|
|
354
|
+
CLOUDY_WITH_PERIODIC_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT = (
|
|
355
|
+
"CLOUDY_WITH_PERIODIC_RAIN_AND_SNOW_RAIN_AS_MAIN_CONSTITUENT"
|
|
356
|
+
)
|
|
357
|
+
CLOUDY_WITH_PERIODIC_RAIN_LATER = "CLOUDY_WITH_PERIODIC_RAIN_LATER"
|
|
358
|
+
CLOUDY_WITH_PERIODIC_SNOW = "CLOUDY_WITH_PERIODIC_SNOW"
|
|
359
|
+
CLOUDY_WITH_PERIODIC_SNOW_ACCOMPANIED_BY_THUNDER = "CLOUDY_WITH_PERIODIC_SNOW_ACCOMPANIED_BY_THUNDER"
|
|
360
|
+
CLOUDY_WITH_PERIODIC_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT = (
|
|
361
|
+
"CLOUDY_WITH_PERIODIC_SNOW_AND_RAIN_SNOW_AS_MAIN_CONSTITUENT"
|
|
362
|
+
)
|
|
363
|
+
CLOUDY_WITH_PERIODIC_SNOW_LATER = "CLOUDY_WITH_PERIODIC_SNOW_LATER"
|
|
364
|
+
CLOUDY_WITH_RAIN_AFTER_MIDNIGHT = "CLOUDY_WITH_RAIN_AFTER_MIDNIGHT"
|
|
365
|
+
CLOUDY_WITH_RAIN_FROM_ABOUT_NOON = "CLOUDY_WITH_RAIN_FROM_ABOUT_NOON"
|
|
366
|
+
CLOUDY_WITH_RAIN_FROM_EVENING = "CLOUDY_WITH_RAIN_FROM_EVENING"
|
|
367
|
+
CLOUDY_WITH_RAIN_LATER = "CLOUDY_WITH_RAIN_LATER"
|
|
368
|
+
CLOUDY_WITH_SNOW_FROM_AROUND_NOON = "CLOUDY_WITH_SNOW_FROM_AROUND_NOON"
|
|
369
|
+
CLOUDY_WITH_SNOW_LATER = "CLOUDY_WITH_SNOW_LATER"
|
|
370
|
+
FOG = "FOG"
|
|
371
|
+
RAINSNOW_MIX_LATER_BECOMING_CLEAR_RAIN_AS_MAIN = "RAINSNOW_MIX_LATER_BECOMING_CLEAR_RAIN_AS_MAIN"
|
|
372
|
+
RAINSNOW_MIX_RAIN_AS_MAIN = "RAINSNOW_MIX_RAIN_AS_MAIN"
|
|
373
|
+
RAINY_BUT_PERIODICALLY_CLEAR = "RAINY_BUT_PERIODICALLY_CLEAR"
|
|
374
|
+
RAIN_ACCOMPANIED_BY_LIGHTNING = "RAIN_ACCOMPANIED_BY_LIGHTNING"
|
|
375
|
+
RAIN_ACCOMPANIED_BY_WINDSTORM = "RAIN_ACCOMPANIED_BY_WINDSTORM"
|
|
376
|
+
RAIN_CLEAR_AT_NIGHT = "RAIN_CLEAR_AT_NIGHT"
|
|
377
|
+
RAIN_CLEAR_FROM_AROUND_EVENING = "RAIN_CLEAR_FROM_AROUND_EVENING"
|
|
378
|
+
RAIN_CLEAR_FROM_AROUND_NOON = "RAIN_CLEAR_FROM_AROUND_NOON"
|
|
379
|
+
RAIN_LATER_BECOMING_CLEAR = "RAIN_LATER_BECOMING_CLEAR"
|
|
380
|
+
RAIN_LATER_BECOMING_CLOUDY = "RAIN_LATER_BECOMING_CLOUDY"
|
|
381
|
+
RAIN_LATER_BECOMING_SNOW = "RAIN_LATER_BECOMING_SNOW"
|
|
382
|
+
RAIN_LATER_WITH_PERIODIC_SNOW = "RAIN_LATER_WITH_PERIODIC_SNOW"
|
|
383
|
+
RAIN_SNOW_AT_NIGHT = "RAIN_SNOW_AT_NIGHT"
|
|
384
|
+
RAIN_SNOW_IN_THE_EVENING = "RAIN_SNOW_IN_THE_EVENING"
|
|
385
|
+
RAIN_TEMPORARILY_BECOMING_SLEET = "RAIN_TEMPORARILY_BECOMING_SLEET"
|
|
386
|
+
RAIN_THAT_PERIODICALLY_STOPS = "RAIN_THAT_PERIODICALLY_STOPS"
|
|
387
|
+
RAIN_WITH_BRIEF_SNOW = "RAIN_WITH_BRIEF_SNOW"
|
|
388
|
+
RAIN_WITH_PERIODIC_SNOW = "RAIN_WITH_PERIODIC_SNOW"
|
|
389
|
+
SNOWRAIN_MIX_LATER_BECOMING_CLEAR_SNOW_AS_MAIN = "SNOWRAIN_MIX_LATER_BECOMING_CLEAR_SNOW_AS_MAIN"
|
|
390
|
+
SNOWRAIN_MIX_LATER_BECOMING_CLOUDY_SNOW_AS_MAIN = "SNOWRAIN_MIX_LATER_BECOMING_CLOUDY_SNOW_AS_MAIN"
|
|
391
|
+
SNOWRAIN_MIX_SNOW_AS_MAIN = "SNOWRAIN_MIX_SNOW_AS_MAIN"
|
|
392
|
+
SNOW_ACCOMPANIED_BY_LIGHTNING = "SNOW_ACCOMPANIED_BY_LIGHTNING"
|
|
393
|
+
SNOW_BECOMING_SLEET = "SNOW_BECOMING_SLEET"
|
|
394
|
+
SNOW_LATER_CLEAR = "SNOW_LATER_CLEAR"
|
|
395
|
+
SNOW_LATER_CLOUDY = "SNOW_LATER_CLOUDY"
|
|
396
|
+
SNOW_LATER_RAIN = "SNOW_LATER_RAIN"
|
|
397
|
+
SNOW_PERIODICALLY_CLEAR = "SNOW_PERIODICALLY_CLEAR"
|
|
398
|
+
SNOW_THAT_PERIODICALLY_STOPS = "SNOW_THAT_PERIODICALLY_STOPS"
|
|
399
|
+
SNOW_TURNING_TO_RAIN_AROUND_NOON = "SNOW_TURNING_TO_RAIN_AROUND_NOON"
|
|
400
|
+
SNOW_TURNING_TO_RAIN_IN_THE_EVENING = "SNOW_TURNING_TO_RAIN_IN_THE_EVENING"
|
|
401
|
+
SNOW_TURNING_TO_RAIN_IN_THE_NIGHT = "SNOW_TURNING_TO_RAIN_IN_THE_NIGHT"
|
|
402
|
+
SNOW_WITH_BRIEF_RAIN = "SNOW_WITH_BRIEF_RAIN"
|
|
403
|
+
SNOW_WITH_BRIEF_SLEET = "SNOW_WITH_BRIEF_SLEET"
|
|
404
|
+
SNOW_WITH_PERIODIC_RAIN = "SNOW_WITH_PERIODIC_RAIN"
|
|
405
|
+
VERY_HOT = "VERY_HOT"
|
|
406
|
+
SLEET = "SLEET"
|
|
407
|
+
HEAVY_RAIN_TO_RAINSTORM = "HEAVY_RAIN_TO_RAINSTORM"
|
|
293
408
|
|
|
294
409
|
icon_base_uri: str = field(metadata={"alias": "iconBaseUri"})
|
|
295
410
|
"""The base URI for the icon not including the file type extension."""
|
|
@@ -312,10 +427,10 @@ class IceThickness(BaseModel):
|
|
|
312
427
|
MILLIMETERS = "MILLIMETERS"
|
|
313
428
|
INCHES = "INCHES"
|
|
314
429
|
|
|
315
|
-
thickness: float
|
|
430
|
+
thickness: float | None = None
|
|
316
431
|
"""The ice thickness value."""
|
|
317
432
|
|
|
318
|
-
unit: Unit
|
|
433
|
+
unit: Unit = Unit.UNIT_UNSPECIFIED
|
|
319
434
|
"""The code that represents the unit used to measure the ice thickness."""
|
|
320
435
|
|
|
321
436
|
|
|
@@ -335,6 +450,9 @@ class CurrentConditionsHistory(BaseModel):
|
|
|
335
450
|
qpf: QuantitativePrecipitationForecast
|
|
336
451
|
"""The amount of precipitation (rain or snow) accumulated over the last 24 hours."""
|
|
337
452
|
|
|
453
|
+
snow_qpf: QuantitativePrecipitationForecast | None = field(default=None, metadata={"alias": "snowQpf"})
|
|
454
|
+
"""The amount of snow accumulation over the last 24 hours, measured as liquid water equivalent."""
|
|
455
|
+
|
|
338
456
|
|
|
339
457
|
@dataclass
|
|
340
458
|
class CurrentConditionsResponse(BaseModel):
|
|
@@ -388,11 +506,11 @@ class CurrentConditionsResponse(BaseModel):
|
|
|
388
506
|
uv_index: int = field(metadata={"alias": "uvIndex"})
|
|
389
507
|
"""The current ultraviolet (UV) index."""
|
|
390
508
|
|
|
391
|
-
thunderstorm_probability: int = field(metadata={"alias": "thunderstormProbability"})
|
|
392
|
-
"""The current thunderstorm probability (0-100)."""
|
|
509
|
+
thunderstorm_probability: int | None = field(default=None, metadata={"alias": "thunderstormProbability"})
|
|
510
|
+
"""The current thunderstorm probability (0-100). Omitted in some regions (e.g. Japan and South Korea)."""
|
|
393
511
|
|
|
394
|
-
cloud_cover: int = field(metadata={"alias": "cloudCover"})
|
|
395
|
-
"""The current percentage of the sky covered by clouds (0-100)."""
|
|
512
|
+
cloud_cover: int | None = field(default=None, metadata={"alias": "cloudCover"})
|
|
513
|
+
"""The current percentage of the sky covered by clouds (0-100). Omitted in some regions (e.g. Japan and South Korea)."""
|
|
396
514
|
|
|
397
515
|
|
|
398
516
|
@dataclass
|
|
@@ -411,7 +529,7 @@ class Date(BaseModel):
|
|
|
411
529
|
|
|
412
530
|
@dataclass
|
|
413
531
|
class ForecastDayPart(BaseModel):
|
|
414
|
-
"""Represents a forecast record for a part of the day (daytime or
|
|
532
|
+
"""Represents a forecast record for a part of the day (daytime, nighttime, or full day)."""
|
|
415
533
|
|
|
416
534
|
interval: Interval
|
|
417
535
|
"""The UTC date and time when this part of the day starts and ends."""
|
|
@@ -431,11 +549,11 @@ class ForecastDayPart(BaseModel):
|
|
|
431
549
|
uv_index: int = field(metadata={"alias": "uvIndex"})
|
|
432
550
|
"""The maximum forecasted ultraviolet (UV) index."""
|
|
433
551
|
|
|
434
|
-
thunderstorm_probability: int = field(metadata={"alias": "thunderstormProbability"})
|
|
435
|
-
"""The average thunderstorm probability."""
|
|
552
|
+
thunderstorm_probability: int | None = field(default=None, metadata={"alias": "thunderstormProbability"})
|
|
553
|
+
"""The average thunderstorm probability. Omitted in some regions (e.g. Japan and South Korea)."""
|
|
436
554
|
|
|
437
|
-
cloud_cover: int = field(metadata={"alias": "cloudCover"})
|
|
438
|
-
"""Average cloud cover percent."""
|
|
555
|
+
cloud_cover: int | None = field(default=None, metadata={"alias": "cloudCover"})
|
|
556
|
+
"""Average cloud cover percent. Omitted in some regions (e.g. Japan and South Korea)."""
|
|
439
557
|
|
|
440
558
|
ice_thickness: IceThickness | None = field(default=None, metadata={"alias": "iceThickness"})
|
|
441
559
|
"""The forecasted ice thickness."""
|
|
@@ -459,7 +577,7 @@ class MoonEvents(BaseModel):
|
|
|
459
577
|
class MoonPhase(ApiStrEnum):
|
|
460
578
|
"""Marks the moon phase (a.k.a. lunar phase)."""
|
|
461
579
|
|
|
462
|
-
|
|
580
|
+
PHASE_UNSPECIFIED = "PHASE_UNSPECIFIED"
|
|
463
581
|
NEW_MOON = "NEW_MOON"
|
|
464
582
|
WAXING_CRESCENT = "WAXING_CRESCENT"
|
|
465
583
|
FIRST_QUARTER = "FIRST_QUARTER"
|
|
@@ -489,12 +607,6 @@ class ForecastDay(BaseModel):
|
|
|
489
607
|
display_date: Date = field(metadata={"alias": "displayDate"})
|
|
490
608
|
"""The local date in the time zone of the location."""
|
|
491
609
|
|
|
492
|
-
daytime_forecast: ForecastDayPart = field(metadata={"alias": "daytimeForecast"})
|
|
493
|
-
"""The forecasted weather conditions for the daytime part of the day."""
|
|
494
|
-
|
|
495
|
-
nighttime_forecast: ForecastDayPart = field(metadata={"alias": "nighttimeForecast"})
|
|
496
|
-
"""The forecasted weather conditions for the nighttime part of the day."""
|
|
497
|
-
|
|
498
610
|
max_temperature: Temperature = field(metadata={"alias": "maxTemperature"})
|
|
499
611
|
"""The maximum (high) temperature throughout the day."""
|
|
500
612
|
|
|
@@ -516,6 +628,15 @@ class ForecastDay(BaseModel):
|
|
|
516
628
|
moon_events: MoonEvents = field(metadata={"alias": "moonEvents"})
|
|
517
629
|
"""The events related to the moon (e.g. moonrise, moonset)."""
|
|
518
630
|
|
|
631
|
+
daytime_forecast: ForecastDayPart | None = field(default=None, metadata={"alias": "daytimeForecast"})
|
|
632
|
+
"""The forecasted weather conditions for the daytime part of the day."""
|
|
633
|
+
|
|
634
|
+
nighttime_forecast: ForecastDayPart | None = field(default=None, metadata={"alias": "nighttimeForecast"})
|
|
635
|
+
"""The forecasted weather conditions for the nighttime part of the day."""
|
|
636
|
+
|
|
637
|
+
full_day_forecast: ForecastDayPart | None = field(default=None, metadata={"alias": "fullDayForecast"})
|
|
638
|
+
"""The forecasted weather conditions for the full day (returned instead of daytime/nighttime in Japan and South Korea)."""
|
|
639
|
+
|
|
519
640
|
ice_thickness: IceThickness | None = field(default=None, metadata={"alias": "iceThickness"})
|
|
520
641
|
"""The accumulated amount of ice throughout the entire day."""
|
|
521
642
|
|
|
@@ -618,11 +739,11 @@ class ForecastHour(BaseModel):
|
|
|
618
739
|
uv_index: int = field(metadata={"alias": "uvIndex"})
|
|
619
740
|
"""The forecasted ultraviolet (UV) index."""
|
|
620
741
|
|
|
621
|
-
thunderstorm_probability: int = field(metadata={"alias": "thunderstormProbability"})
|
|
622
|
-
"""The forecasted thunderstorm probability (0-100)."""
|
|
742
|
+
thunderstorm_probability: int | None = field(default=None, metadata={"alias": "thunderstormProbability"})
|
|
743
|
+
"""The forecasted thunderstorm probability (0-100). Omitted in some regions (e.g. Japan and South Korea)."""
|
|
623
744
|
|
|
624
|
-
cloud_cover: int = field(metadata={"alias": "cloudCover"})
|
|
625
|
-
"""The forecasted percentage of the sky covered by clouds (0-100)."""
|
|
745
|
+
cloud_cover: int | None = field(default=None, metadata={"alias": "cloudCover"})
|
|
746
|
+
"""The forecasted percentage of the sky covered by clouds (0-100). Omitted in some regions (e.g. Japan and South Korea)."""
|
|
626
747
|
|
|
627
748
|
ice_thickness: IceThickness | None = field(default=None, metadata={"alias": "iceThickness"})
|
|
628
749
|
"""The forecasted ice thickness."""
|
|
@@ -94,8 +94,12 @@ async def test_get_daily_forecast(api: GoogleWeatherApi, mock_server: MockServer
|
|
|
94
94
|
|
|
95
95
|
assert len(result.forecast_days) == 2
|
|
96
96
|
day = result.forecast_days[0]
|
|
97
|
+
assert day.max_temperature.degrees is not None
|
|
98
|
+
assert day.min_temperature.degrees is not None
|
|
97
99
|
assert day.max_temperature.degrees >= day.min_temperature.degrees
|
|
100
|
+
assert day.daytime_forecast is not None
|
|
98
101
|
assert day.daytime_forecast.weather_condition.type
|
|
102
|
+
assert day.nighttime_forecast is not None
|
|
99
103
|
assert day.nighttime_forecast.weather_condition.type
|
|
100
104
|
assert day.sun_events.sunrise_time
|
|
101
105
|
assert day.moon_events.moon_phase
|
|
@@ -332,6 +336,14 @@ async def test_get_minute_forecast(api: GoogleWeatherApi, mock_server: MockServe
|
|
|
332
336
|
assert "language_code" not in request.query
|
|
333
337
|
|
|
334
338
|
|
|
339
|
+
async def test_malformed_response_raises_response_error(api: GoogleWeatherApi, mock_server: MockServer) -> None:
|
|
340
|
+
"""Test a 200 OK response missing required schema fields raises GoogleWeatherApiResponseError."""
|
|
341
|
+
mock_server.add_response({})
|
|
342
|
+
|
|
343
|
+
with pytest.raises(GoogleWeatherApiResponseError):
|
|
344
|
+
await api.async_get_current_conditions(LATITUDE, LONGITUDE)
|
|
345
|
+
|
|
346
|
+
|
|
335
347
|
def _load_dotenv() -> dict[str, str]:
|
|
336
348
|
"""Load credentials from .env or environment variables."""
|
|
337
349
|
env: dict[str, str] = {}
|
|
@@ -372,3 +384,19 @@ async def test_live_weather_endpoints(session: aiohttp.ClientSession) -> None:
|
|
|
372
384
|
|
|
373
385
|
minute = await client.async_get_minute_forecast(LATITUDE, LONGITUDE, page_size=10)
|
|
374
386
|
assert minute.time_zone.id
|
|
387
|
+
|
|
388
|
+
# Japan (Tokyo) and South Korea (Seoul) use fullDayForecast and omit several fields.
|
|
389
|
+
for lat, lon, expected_tz in (
|
|
390
|
+
(35.6762, 139.6503, "Asia/Tokyo"),
|
|
391
|
+
(37.5665, 126.9780, "Asia/Seoul"),
|
|
392
|
+
):
|
|
393
|
+
regional_current = await client.async_get_current_conditions(lat, lon)
|
|
394
|
+
assert regional_current.time_zone.id == expected_tz
|
|
395
|
+
assert regional_current.temperature.degrees is not None
|
|
396
|
+
|
|
397
|
+
regional_daily = await client.async_get_daily_forecast(lat, lon, days=2)
|
|
398
|
+
assert len(regional_daily.forecast_days) == 2
|
|
399
|
+
assert regional_daily.forecast_days[0].full_day_forecast is not None
|
|
400
|
+
|
|
401
|
+
regional_hourly = await client.async_get_hourly_forecast(lat, lon, hours=2)
|
|
402
|
+
assert len(regional_hourly.forecast_hours) == 2
|
|
@@ -135,6 +135,7 @@ def test_optional_fields_may_be_absent() -> None:
|
|
|
135
135
|
assert parsed.moon_events.moonrise_times == []
|
|
136
136
|
assert parsed.moon_events.moonset_times == []
|
|
137
137
|
assert parsed.ice_thickness is None
|
|
138
|
+
assert parsed.daytime_forecast is not None
|
|
138
139
|
assert parsed.daytime_forecast.ice_thickness is None
|
|
139
140
|
assert parsed.daytime_forecast.precipitation.snow_qpf is None
|
|
140
141
|
assert parsed.interval.end_time is None
|
|
@@ -186,3 +187,104 @@ def test_daily_and_hourly_forecasts_without_records() -> None:
|
|
|
186
187
|
hourly = HourlyForecastResponse.from_dict({"timeZone": {"id": "America/New_York"}})
|
|
187
188
|
assert hourly.forecast_hours == []
|
|
188
189
|
assert "nextPageToken" not in hourly.to_dict()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_japan_and_south_korea_current_conditions(caplog: pytest.LogCaptureFixture) -> None:
|
|
193
|
+
"""Test parsing current conditions with Japan/South Korea omitted and UNIT_UNSPECIFIED fields."""
|
|
194
|
+
data = load_fixture("current_conditions")
|
|
195
|
+
data["timeZone"] = {"id": "Asia/Tokyo", "version": ""}
|
|
196
|
+
data["heatIndex"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
197
|
+
data["windChill"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
198
|
+
data["visibility"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
199
|
+
data["precipitation"]["snowQpf"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
200
|
+
data["currentConditionsHistory"]["snowQpf"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
201
|
+
data["wind"].pop("gust", None)
|
|
202
|
+
data.pop("cloudCover", None)
|
|
203
|
+
data.pop("thunderstormProbability", None)
|
|
204
|
+
|
|
205
|
+
with caplog.at_level(logging.WARNING, logger="google_weather_api"):
|
|
206
|
+
parsed = CurrentConditionsResponse.from_dict(data)
|
|
207
|
+
|
|
208
|
+
assert caplog.records == []
|
|
209
|
+
assert parsed.heat_index.degrees is None
|
|
210
|
+
assert parsed.heat_index.unit is Temperature.TemperatureUnit.UNIT_UNSPECIFIED
|
|
211
|
+
assert parsed.wind_chill.degrees is None
|
|
212
|
+
assert parsed.visibility.distance is None
|
|
213
|
+
assert parsed.visibility.unit is Visibility.Unit.UNIT_UNSPECIFIED
|
|
214
|
+
assert parsed.precipitation.snow_qpf is not None
|
|
215
|
+
assert parsed.precipitation.snow_qpf.quantity is None
|
|
216
|
+
assert parsed.current_conditions_history.snow_qpf is not None
|
|
217
|
+
assert parsed.current_conditions_history.snow_qpf.quantity is None
|
|
218
|
+
assert parsed.wind.gust is None
|
|
219
|
+
assert parsed.cloud_cover is None
|
|
220
|
+
assert parsed.thunderstorm_probability is None
|
|
221
|
+
assert CurrentConditionsResponse.from_dict(parsed.to_dict()) == parsed
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_japan_and_south_korea_daily_forecast(caplog: pytest.LogCaptureFixture) -> None:
|
|
225
|
+
"""Test parsing Japan/South Korea fullDayForecast, regional condition types, and omitted fields."""
|
|
226
|
+
daily_data = load_fixture("daily_forecast")
|
|
227
|
+
day: dict[str, Any] = daily_data["forecastDays"][0]
|
|
228
|
+
full_day = day.pop("daytimeForecast")
|
|
229
|
+
day.pop("nighttimeForecast")
|
|
230
|
+
full_day["weatherCondition"]["type"] = "CLOUDY_PERIODICALLY_CLEAR"
|
|
231
|
+
full_day["wind"].pop("gust", None)
|
|
232
|
+
full_day.pop("cloudCover", None)
|
|
233
|
+
full_day.pop("thunderstormProbability", None)
|
|
234
|
+
full_day["iceThickness"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
235
|
+
full_day["precipitation"]["snowQpf"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
236
|
+
day["fullDayForecast"] = full_day
|
|
237
|
+
day["feelsLikeMaxTemperature"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
238
|
+
day["feelsLikeMinTemperature"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
239
|
+
day["maxHeatIndex"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
240
|
+
day.pop("iceThickness", None)
|
|
241
|
+
|
|
242
|
+
with caplog.at_level(logging.WARNING, logger="google_weather_api"):
|
|
243
|
+
parsed_daily = DailyForecastResponse.from_dict(daily_data)
|
|
244
|
+
|
|
245
|
+
assert caplog.records == []
|
|
246
|
+
parsed_day = parsed_daily.forecast_days[0]
|
|
247
|
+
assert parsed_day.daytime_forecast is None
|
|
248
|
+
assert parsed_day.nighttime_forecast is None
|
|
249
|
+
assert parsed_day.full_day_forecast is not None
|
|
250
|
+
assert parsed_day.full_day_forecast.weather_condition.type is WeatherCondition.Type.CLOUDY_PERIODICALLY_CLEAR
|
|
251
|
+
assert parsed_day.full_day_forecast.wind.gust is None
|
|
252
|
+
assert parsed_day.full_day_forecast.cloud_cover is None
|
|
253
|
+
assert parsed_day.full_day_forecast.thunderstorm_probability is None
|
|
254
|
+
assert parsed_day.full_day_forecast.ice_thickness is not None
|
|
255
|
+
assert parsed_day.full_day_forecast.ice_thickness.thickness is None
|
|
256
|
+
assert parsed_day.feels_like_max_temperature.degrees is None
|
|
257
|
+
assert parsed_day.feels_like_min_temperature.degrees is None
|
|
258
|
+
assert parsed_day.max_heat_index.degrees is None
|
|
259
|
+
assert DailyForecastResponse.from_dict(parsed_daily.to_dict()) == parsed_daily
|
|
260
|
+
assert len(WeatherCondition.Type) == 139
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_japan_and_south_korea_hourly_forecast(caplog: pytest.LogCaptureFixture) -> None:
|
|
264
|
+
"""Test parsing Japan/South Korea hourly forecast with omitted and UNIT_UNSPECIFIED fields."""
|
|
265
|
+
hourly_data = load_fixture("hourly_forecast_page1")
|
|
266
|
+
hour: dict[str, Any] = hourly_data["forecastHours"][0]
|
|
267
|
+
hour["heatIndex"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
268
|
+
hour["windChill"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
269
|
+
hour["wetBulbTemperature"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
270
|
+
hour["visibility"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
271
|
+
hour["iceThickness"] = {"unit": "UNIT_UNSPECIFIED"}
|
|
272
|
+
hour["wind"].pop("gust", None)
|
|
273
|
+
hour.pop("cloudCover", None)
|
|
274
|
+
hour.pop("thunderstormProbability", None)
|
|
275
|
+
|
|
276
|
+
with caplog.at_level(logging.WARNING, logger="google_weather_api"):
|
|
277
|
+
parsed_hourly = HourlyForecastResponse.from_dict(hourly_data)
|
|
278
|
+
|
|
279
|
+
assert caplog.records == []
|
|
280
|
+
parsed_hour = parsed_hourly.forecast_hours[0]
|
|
281
|
+
assert parsed_hour.heat_index.degrees is None
|
|
282
|
+
assert parsed_hour.wind_chill.degrees is None
|
|
283
|
+
assert parsed_hour.wet_bulb_temperature.degrees is None
|
|
284
|
+
assert parsed_hour.visibility.distance is None
|
|
285
|
+
assert parsed_hour.ice_thickness is not None
|
|
286
|
+
assert parsed_hour.ice_thickness.thickness is None
|
|
287
|
+
assert parsed_hour.wind.gust is None
|
|
288
|
+
assert parsed_hour.cloud_cover is None
|
|
289
|
+
assert parsed_hour.thunderstorm_probability is None
|
|
290
|
+
assert HourlyForecastResponse.from_dict(parsed_hourly.to_dict()) == parsed_hourly
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|