python-google-weather-api 0.0.5__tar.gz → 0.0.7__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 (18) hide show
  1. {python_google_weather_api-0.0.5/src/python_google_weather_api.egg-info → python_google_weather_api-0.0.7}/PKG-INFO +3 -1
  2. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/pyproject.toml +11 -1
  3. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/google_weather_api/api.py +40 -7
  4. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/google_weather_api/exceptions.py +17 -17
  5. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/google_weather_api/model.py +641 -605
  6. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7/src/python_google_weather_api.egg-info}/PKG-INFO +3 -1
  7. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/python_google_weather_api.egg-info/SOURCES.txt +2 -1
  8. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/python_google_weather_api.egg-info/requires.txt +2 -0
  9. python_google_weather_api-0.0.7/tests/test_api.py +269 -0
  10. python_google_weather_api-0.0.7/tests/test_model.py +143 -0
  11. python_google_weather_api-0.0.5/tests/test_api.py +0 -6
  12. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/LICENSE +0 -0
  13. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/README.md +0 -0
  14. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/setup.cfg +0 -0
  15. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/google_weather_api/__init__.py +0 -0
  16. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/google_weather_api/py.typed +0 -0
  17. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/src/python_google_weather_api.egg-info/dependency_links.txt +0 -0
  18. {python_google_weather_api-0.0.5 → python_google_weather_api-0.0.7}/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.5
3
+ Version: 0.0.7
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
@@ -13,6 +13,8 @@ Requires-Dist: aiohttp>=3.8
13
13
  Requires-Dist: mashumaro
14
14
  Provides-Extra: test
15
15
  Requires-Dist: pytest; extra == "test"
16
+ Requires-Dist: pytest-asyncio; extra == "test"
17
+ Requires-Dist: pytest-cov; extra == "test"
16
18
  Dynamic: license-file
17
19
 
18
20
  # python-google-weather-api
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-google-weather-api"
3
- version = "0.0.5"
3
+ version = "0.0.7"
4
4
  license = "Apache-2.0"
5
5
  authors = [
6
6
  { name="tronikos", email="tronikos@gmail.com" },
@@ -16,6 +16,8 @@ dependencies = [
16
16
  [project.optional-dependencies]
17
17
  test = [
18
18
  "pytest",
19
+ "pytest-asyncio",
20
+ "pytest-cov",
19
21
  ]
20
22
 
21
23
 
@@ -27,6 +29,13 @@ test = [
27
29
  requires = ["setuptools"]
28
30
  build-backend = "setuptools.build_meta"
29
31
 
32
+ [tool.pytest.ini_options]
33
+ asyncio_mode = "auto"
34
+ addopts = "--cov=google_weather_api --cov-report=term-missing --cov-fail-under=90"
35
+
36
+ [tool.coverage.run]
37
+ branch = true
38
+
30
39
  [tool.ruff]
31
40
  target-version = "py311"
32
41
  line-length = 127
@@ -64,6 +73,7 @@ ignore = [
64
73
  "D203", # 1 blank line required before class docstring
65
74
  "D213", # Multi-line docstring summary should start at the second line
66
75
  "PLR0913", # Too many arguments in function definition
76
+ "PLR0917", # Too many positional arguments in function definition
67
77
  "PLR1714", # Consider merging multiple comparisons
68
78
  "PLR2004", # Magic value used in comparison
69
79
  "TRY003", # Avoid specifying long messages outside the exception class
@@ -53,11 +53,12 @@ class GoogleWeatherApi:
53
53
  headers[aiohttp.hdrs.REFERER] = self.referrer
54
54
  params = {
55
55
  **params,
56
- "key": self.api_key,
57
56
  "language_code": self.language_code,
58
57
  "units_system": self.units_system,
59
58
  }
59
+ # Log before adding the API key so it doesn't end up in debug logs.
60
60
  _LOGGER.debug("GET %s with params: %s", url, params)
61
+ params["key"] = self.api_key
61
62
  try:
62
63
  async with self.session.get(
63
64
  url,
@@ -68,15 +69,45 @@ class GoogleWeatherApi:
68
69
  res: dict[str, Any] = await resp.json()
69
70
  _LOGGER.debug("Got %s for %s", resp.status, url)
70
71
  if resp.status != HTTPStatus.OK:
72
+ error_data = res.get("error", {})
73
+ error_msg = error_data.get("message", "Unknown API error")
74
+
71
75
  if resp.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
72
- raise GoogleWeatherApiAuthError(res["error"]["message"])
73
- raise GoogleWeatherApiResponseError(res["error"]["message"])
76
+ raise GoogleWeatherApiAuthError(error_msg)
77
+
78
+ # Google APIs often return 400 Bad Request for invalid API keys
79
+ if resp.status == HTTPStatus.BAD_REQUEST:
80
+ for detail in error_data.get("details", []):
81
+ if detail.get("reason") == "API_KEY_INVALID":
82
+ raise GoogleWeatherApiAuthError(error_msg)
83
+
84
+ raise GoogleWeatherApiResponseError(error_msg)
74
85
  return res
75
86
  except TimeoutError as err:
76
87
  raise GoogleWeatherApiConnectionError("Timeout") from err
77
88
  except aiohttp.ClientError as err:
78
89
  raise GoogleWeatherApiConnectionError(err) from err
79
90
 
91
+ async def _async_get_all_pages(
92
+ self, endpoint: str, params: dict[str, Any], records_key: str, limit: int
93
+ ) -> dict[str, Any]:
94
+ """Perform a GET request, following pagination until `limit` records are collected.
95
+
96
+ The API caps the number of records it returns per page (24 for hourly forecasts),
97
+ regardless of the requested page size, so a single request is not enough.
98
+ """
99
+ params = {**params, "page_size": limit}
100
+ data = await self._async_get(endpoint, params)
101
+ records: list[Any] = data.get(records_key, [])
102
+ while len(records) < limit and (page_token := data.get("nextPageToken")):
103
+ data = await self._async_get(endpoint, {**params, "page_token": page_token})
104
+ page_records: list[Any] = data.get(records_key, [])
105
+ if not page_records:
106
+ break
107
+ records.extend(page_records)
108
+ data[records_key] = records[:limit]
109
+ return data
110
+
80
111
  async def async_get_current_conditions(self, latitude: float, longitude: float) -> CurrentConditionsResponse:
81
112
  """Fetch current weather conditions.
82
113
 
@@ -96,14 +127,15 @@ class GoogleWeatherApi:
96
127
 
97
128
  See https://developers.google.com/maps/documentation/weather/reference/rest/v1/forecast.hours/lookup
98
129
  """
99
- data = await self._async_get(
130
+ data = await self._async_get_all_pages(
100
131
  "forecast/hours:lookup",
101
132
  {
102
133
  "location.latitude": latitude,
103
134
  "location.longitude": longitude,
104
135
  "hours": hours,
105
- "page_size": hours,
106
136
  },
137
+ "forecastHours",
138
+ hours,
107
139
  )
108
140
  return HourlyForecastResponse.from_dict(data)
109
141
 
@@ -112,13 +144,14 @@ class GoogleWeatherApi:
112
144
 
113
145
  See https://developers.google.com/maps/documentation/weather/reference/rest/v1/forecast.days/lookup
114
146
  """
115
- data = await self._async_get(
147
+ data = await self._async_get_all_pages(
116
148
  "forecast/days:lookup",
117
149
  {
118
150
  "location.latitude": latitude,
119
151
  "location.longitude": longitude,
120
152
  "days": days,
121
- "page_size": days,
122
153
  },
154
+ "forecastDays",
155
+ days,
123
156
  )
124
157
  return DailyForecastResponse.from_dict(data)
@@ -1,17 +1,17 @@
1
- """Exceptions for Google Weather API."""
2
-
3
-
4
- class GoogleWeatherApiError(Exception):
5
- """Exception talking to the Google Weather API."""
6
-
7
-
8
- class GoogleWeatherApiConnectionError(GoogleWeatherApiError):
9
- """Exception connecting to the Google Weather API."""
10
-
11
-
12
- class GoogleWeatherApiAuthError(GoogleWeatherApiError):
13
- """Exception raised for authentication errors in the Google Weather API."""
14
-
15
-
16
- class GoogleWeatherApiResponseError(GoogleWeatherApiError):
17
- """Exception raised for errors in the Google Weather API response."""
1
+ """Exceptions for Google Weather API."""
2
+
3
+
4
+ class GoogleWeatherApiError(Exception):
5
+ """Exception talking to the Google Weather API."""
6
+
7
+
8
+ class GoogleWeatherApiConnectionError(GoogleWeatherApiError):
9
+ """Exception connecting to the Google Weather API."""
10
+
11
+
12
+ class GoogleWeatherApiAuthError(GoogleWeatherApiError):
13
+ """Exception raised for authentication errors in the Google Weather API."""
14
+
15
+
16
+ class GoogleWeatherApiResponseError(GoogleWeatherApiError):
17
+ """Exception raised for errors in the Google Weather API response."""