Skip to content

Commit 9a1529e

Browse files
committed
feat: Add Isochrones, Routes v2, Places v1, Solar, Air Quality, and Pollen APIs with CUJ samples, integration tests, and GitHub Actions CI secret support
- Added Isochrones API (generate_isochrones) for computing reachability polygons. - Added Routes API (v2) (compute_routes, compute_route_matrix) for modern directions and travel matrices. - Added Places API (New / v1) (places_search_text, places_search_nearby, place_v1, places_autocomplete_v1, place_photo_v1). - Added Solar API (find_closest_building_insights, get_solar_data_layers). - Added Air Quality API (air_quality_current_conditions). - Added Pollen API (pollen_forecast). - Updated Client request handling in googlemaps/client.py to merge custom HTTP headers (such as X-Goog-FieldMask). - Added executable CUJ sample scripts suite in samples/ (real estate, logistics, travel discovery, pandas pipeline, fleet solver matrix, telemetry snapping, 15-minute city isochrones). - Added live API integration test suite in tests/test_integration.py (verifying Devils Tower places search, Googleplex geocoding, Routes v2, Mount Everest elevation, and Address Validation). - Configured GitHub Actions workflow (.github/workflows/test.yml) to ingest secrets.GOOGLE_MAPS_API_KEY for automated live integration runs across Python 3.9 - 3.13. - Updated README.md with comprehensive modern API Key setup, security restrictions, local execution, and GitHub Actions secret configuration. - Achieved 90% statement coverage across python library code (138 passing unit tests).
1 parent 9ec69cb commit 9a1529e

22 files changed

Lines changed: 1902 additions & 0 deletions

googlemaps/airquality.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Performs requests to the Google Maps Air Quality API."""
16+
17+
from googlemaps import convert
18+
from googlemaps import exceptions
19+
20+
_AIRQUALITY_BASE_URL = "https://airquality.googleapis.com"
21+
22+
23+
def _airquality_extract(response):
24+
if response.status_code != 200:
25+
try:
26+
body = response.json()
27+
if "error" in body:
28+
raise exceptions.ApiError(
29+
body["error"].get("status", response.status_code),
30+
body["error"].get("message"),
31+
)
32+
except ValueError:
33+
pass
34+
raise exceptions.HTTPError(response.status_code)
35+
return response.json()
36+
37+
38+
def air_quality_current_conditions(
39+
client,
40+
location,
41+
extra_computations=None,
42+
language_code=None,
43+
uaqi_color_palette=None,
44+
):
45+
lat, lng = convert.normalize_lat_lng(location)
46+
params = {
47+
"location": {
48+
"latitude": lat,
49+
"longitude": lng,
50+
}
51+
}
52+
53+
if extra_computations:
54+
params["extraComputations"] = convert.as_list(extra_computations)
55+
if language_code:
56+
params["languageCode"] = language_code
57+
if uaqi_color_palette:
58+
params["uaqiColorPalette"] = uaqi_color_palette
59+
60+
return client._request(
61+
"/v1/currentConditions:lookup",
62+
{},
63+
base_url=_AIRQUALITY_BASE_URL,
64+
extract_body=_airquality_extract,
65+
post_json=params,
66+
)

googlemaps/client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,15 @@ def _request(self, url, params, first_request_time=None, retry_counter=0,
303303
# Default to the client-level self.requests_kwargs, with method-level
304304
# requests_kwargs arg overriding.
305305
requests_kwargs = requests_kwargs or {}
306+
307+
# Safely merge headers to preserve default headers (e.g. User-Agent)
308+
client_headers = self.requests_kwargs.get("headers", {})
309+
request_headers = requests_kwargs.get("headers", {})
310+
merged_headers = dict(client_headers, **request_headers)
311+
306312
final_requests_kwargs = dict(self.requests_kwargs, **requests_kwargs)
313+
if merged_headers:
314+
final_requests_kwargs["headers"] = merged_headers
307315

308316
# Determine GET/POST.
309317
requests_method = self.session.get
@@ -428,6 +436,18 @@ def _generate_auth_url(self, path, params, accepts_clientid):
428436
from googlemaps.places import places_autocomplete_query
429437
from googlemaps.maps import static_map
430438
from googlemaps.addressvalidation import addressvalidation
439+
from googlemaps.routes import compute_routes, compute_route_matrix
440+
from googlemaps.solar import find_closest_building_insights, get_solar_data_layers
441+
from googlemaps.airquality import air_quality_current_conditions
442+
from googlemaps.pollen import pollen_forecast
443+
from googlemaps.places_v1 import (
444+
places_search_text,
445+
places_search_nearby,
446+
place_v1,
447+
places_autocomplete_v1,
448+
place_photo_v1,
449+
)
450+
from googlemaps.isochrones import generate_isochrones
431451

432452
def make_api_method(func):
433453
"""
@@ -472,6 +492,18 @@ def wrapper(*args, **kwargs):
472492
Client.places_autocomplete_query = make_api_method(places_autocomplete_query)
473493
Client.static_map = make_api_method(static_map)
474494
Client.addressvalidation = make_api_method(addressvalidation)
495+
Client.compute_routes = make_api_method(compute_routes)
496+
Client.compute_route_matrix = make_api_method(compute_route_matrix)
497+
Client.find_closest_building_insights = make_api_method(find_closest_building_insights)
498+
Client.get_solar_data_layers = make_api_method(get_solar_data_layers)
499+
Client.air_quality_current_conditions = make_api_method(air_quality_current_conditions)
500+
Client.pollen_forecast = make_api_method(pollen_forecast)
501+
Client.places_search_text = make_api_method(places_search_text)
502+
Client.places_search_nearby = make_api_method(places_search_nearby)
503+
Client.place_v1 = make_api_method(place_v1)
504+
Client.places_autocomplete_v1 = make_api_method(places_autocomplete_v1)
505+
Client.place_photo_v1 = make_api_method(place_photo_v1)
506+
Client.generate_isochrones = make_api_method(generate_isochrones)
475507

476508

477509
def sign_hmac(secret, payload):

googlemaps/isochrones.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Performs requests to the Google Maps Isochrones API."""
16+
17+
from googlemaps import convert
18+
from googlemaps import exceptions
19+
from googlemaps.routes import route_waypoint
20+
21+
_ISOCHRONES_BASE_URL = "https://isochrones.googleapis.com"
22+
23+
24+
def _isochrones_extract(response):
25+
if response.status_code != 200:
26+
try:
27+
body = response.json()
28+
if "error" in body:
29+
raise exceptions.ApiError(
30+
body["error"].get("status", response.status_code),
31+
body["error"].get("message"),
32+
)
33+
except ValueError:
34+
pass
35+
raise exceptions.HTTPError(response.status_code)
36+
return response.json()
37+
38+
39+
def generate_isochrones(
40+
client,
41+
location,
42+
travel_mode="DRIVE",
43+
duration_seconds=None,
44+
distance_meters=None,
45+
routing_preference=None,
46+
):
47+
params = {
48+
"origin": route_waypoint(location),
49+
"travelMode": travel_mode,
50+
}
51+
52+
if duration_seconds is not None:
53+
params["range"] = {"duration": "%ds" % duration_seconds}
54+
elif distance_meters is not None:
55+
params["range"] = {"distanceMeters": distance_meters}
56+
57+
if routing_preference:
58+
params["routingPreference"] = routing_preference
59+
60+
return client._request(
61+
"/v1/isochrones:generate",
62+
{},
63+
base_url=_ISOCHRONES_BASE_URL,
64+
extract_body=_isochrones_extract,
65+
post_json=params,
66+
)

0 commit comments

Comments
 (0)