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.
Files changed (17) hide show
  1. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/PKG-INFO +1 -1
  2. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/pyproject.toml +1 -1
  3. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/api.py +18 -5
  4. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/model.py +156 -35
  5. {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
  6. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/tests/test_api.py +28 -0
  7. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/tests/test_model.py +102 -0
  8. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/LICENSE +0 -0
  9. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/README.md +0 -0
  10. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/setup.cfg +0 -0
  11. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/__init__.py +0 -0
  12. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/exceptions.py +0 -0
  13. {python_google_weather_api-0.0.10 → python_google_weather_api-0.0.11}/src/google_weather_api/py.typed +0 -0
  14. {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
  15. {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
  16. {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
  17. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-google-weather-api
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: A python client library for Google Weather API
5
5
  Author-email: tronikos <tronikos@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-google-weather-api"
3
- version = "0.0.10"
3
+ version = "0.0.11"
4
4
  license = "Apache-2.0"
5
5
  authors = [
6
6
  { name="tronikos", email="tronikos@gmail.com" },
@@ -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.from_dict(data)
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.from_dict(data)
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.from_dict(data)
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.from_dict(data)
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
- TEMPERATURE_UNIT_UNSPECIFIED = "TEMPERATURE_UNIT_UNSPECIFIED"
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
- PRECIPITATION_TYPE_UNSPECIFIED = "PRECIPITATION_TYPE_UNSPECIFIED"
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
- SPEED_UNIT_UNSPECIFIED = "SPEED_UNIT_UNSPECIFIED"
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
- CARDINAL_DIRECTION_UNSPECIFIED = "CARDINAL_DIRECTION_UNSPECIFIED"
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 nighttime)."""
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
- MOON_PHASE_UNSPECIFIED = "MOON_PHASE_UNSPECIFIED"
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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-google-weather-api
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: A python client library for Google Weather API
5
5
  Author-email: tronikos <tronikos@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -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