diff --git a/googlemaps/client.py b/googlemaps/client.py index 26bd17b2..72b59ed0 100644 --- a/googlemaps/client.py +++ b/googlemaps/client.py @@ -152,10 +152,10 @@ def __init__(self, key=None, client_id=None, client_secret=None, self.queries_per_second = queries_per_second self.sent_times = collections.deque("", queries_per_second) - def _get(self, url, params, first_request_time=None, retry_counter=0, + def _request(self, url, params, first_request_time=None, retry_counter=0, base_url=_DEFAULT_BASE_URL, accepts_clientid=True, - extract_body=None, requests_kwargs=None): - """Performs HTTP GET request with credentials, returning the body as + extract_body=None, requests_kwargs=None, post_json=None): + """Performs HTTP GET/POST with credentials, returning the body as JSON. :param url: URL path for the request. Should begin with a slash. @@ -215,21 +215,32 @@ def _get(self, url, params, first_request_time=None, retry_counter=0, # Default to the client-level self.requests_kwargs, with method-level # requests_kwargs arg overriding. - requests_kwargs = dict(self.requests_kwargs, **(requests_kwargs or {})) + requests_kwargs = requests_kwargs or {} + final_requests_kwargs = dict(self.requests_kwargs, **requests_kwargs) + + # Determine GET/POST. + requests_method = self.session.get + if post_json is not None: + requests_method = self.session.post + final_requests_kwargs["json"] = post_json + try: - resp = self.session.get(base_url + authed_url, **requests_kwargs) + response = requests_method(base_url + authed_url, + **final_requests_kwargs) except requests.exceptions.Timeout: raise googlemaps.exceptions.Timeout() except Exception as e: raise googlemaps.exceptions.TransportError(e) - if resp.status_code in _RETRIABLE_STATUSES: + if response.status_code in _RETRIABLE_STATUSES: # Retry request. - return self._get(url, params, first_request_time, retry_counter + 1, - base_url, accepts_clientid, extract_body) + return self._request(url, params, first_request_time, + retry_counter + 1, base_url, accepts_clientid, + extract_body, requests_kwargs, post_json) - # Check if the time of the nth previous query (where n is queries_per_second) - # is under a second ago - if so, sleep for the difference. + # Check if the time of the nth previous query (where n is + # queries_per_second) is under a second ago - if so, sleep for + # the difference. if self.sent_times and len(self.sent_times) == self.queries_per_second: elapsed_since_earliest = time.time() - self.sent_times[0] if elapsed_since_earliest < 1: @@ -237,21 +248,25 @@ def _get(self, url, params, first_request_time=None, retry_counter=0, try: if extract_body: - result = extract_body(resp) + result = extract_body(response) else: - result = self._get_body(resp) + result = self._get_body(response) self.sent_times.append(time.time()) return result except googlemaps.exceptions._RetriableRequest: # Retry request. - return self._get(url, params, first_request_time, retry_counter + 1, - base_url, accepts_clientid, extract_body) + return self._request(url, params, first_request_time, + retry_counter + 1, base_url, accepts_clientid, + extract_body, requests_kwargs, post_json) + + def _get(self, *args, **kwargs): # Backwards compatibility. + return self._request(*args, **kwargs) - def _get_body(self, resp): - if resp.status_code != 200: - raise googlemaps.exceptions.HTTPError(resp.status_code) + def _get_body(self, response): + if response.status_code != 200: + raise googlemaps.exceptions.HTTPError(response.status_code) - body = resp.json() + body = response.json() api_status = body["status"] if api_status == "OK" or api_status == "ZERO_RESULTS": @@ -310,6 +325,7 @@ def _generate_auth_url(self, path, params, accepts_clientid): from googlemaps.elevation import elevation_along_path from googlemaps.geocoding import geocode from googlemaps.geocoding import reverse_geocode +from googlemaps.geolocation import geolocate from googlemaps.timezone import timezone from googlemaps.roads import snap_to_roads from googlemaps.roads import nearest_roads @@ -352,6 +368,7 @@ def wrapper(*args, **kwargs): Client.elevation_along_path = make_api_method(elevation_along_path) Client.geocode = make_api_method(geocode) Client.reverse_geocode = make_api_method(reverse_geocode) +Client.geolocate = make_api_method(geolocate) Client.timezone = make_api_method(timezone) Client.snap_to_roads = make_api_method(snap_to_roads) Client.nearest_roads = make_api_method(nearest_roads) diff --git a/googlemaps/directions.py b/googlemaps/directions.py index cb939109..236dc96a 100644 --- a/googlemaps/directions.py +++ b/googlemaps/directions.py @@ -147,4 +147,4 @@ def directions(client, origin, destination, if traffic_model: params["traffic_model"] = traffic_model - return client._get("/maps/api/directions/json", params)["routes"] + return client._request("/maps/api/directions/json", params)["routes"] diff --git a/googlemaps/distance_matrix.py b/googlemaps/distance_matrix.py index 89fb23a1..6c1f0626 100644 --- a/googlemaps/distance_matrix.py +++ b/googlemaps/distance_matrix.py @@ -127,4 +127,4 @@ def distance_matrix(client, origins, destinations, if traffic_model: params["traffic_model"] = traffic_model - return client._get("/maps/api/distancematrix/json", params) + return client._request("/maps/api/distancematrix/json", params) diff --git a/googlemaps/elevation.py b/googlemaps/elevation.py index 4b286f06..4546679c 100644 --- a/googlemaps/elevation.py +++ b/googlemaps/elevation.py @@ -34,7 +34,7 @@ def elevation(client, locations): :rtype: list of elevation data responses """ params = {"locations": convert.shortest_path(locations)} - return client._get("/maps/api/elevation/json", params)["results"] + return client._request("/maps/api/elevation/json", params)["results"] def elevation_along_path(client, path, samples): @@ -62,4 +62,4 @@ def elevation_along_path(client, path, samples): "samples": samples } - return client._get("/maps/api/elevation/json", params)["results"] + return client._request("/maps/api/elevation/json", params)["results"] diff --git a/googlemaps/geocoding.py b/googlemaps/geocoding.py index d0a789a8..99ceeeff 100644 --- a/googlemaps/geocoding.py +++ b/googlemaps/geocoding.py @@ -65,7 +65,7 @@ def geocode(client, address=None, components=None, bounds=None, region=None, if language: params["language"] = language - return client._get("/maps/api/geocode/json", params)["results"] + return client._request("/maps/api/geocode/json", params)["results"] def reverse_geocode(client, latlng, result_type=None, location_type=None, @@ -77,7 +77,7 @@ def reverse_geocode(client, latlng, result_type=None, location_type=None, :param latlng: The latitude/longitude value or place_id for which you wish to obtain the closest, human-readable address. :type latlng: string, dict, list, or tuple - + :param result_type: One or more address types to restrict results to. :type result_type: string or list of strings @@ -106,4 +106,4 @@ def reverse_geocode(client, latlng, result_type=None, location_type=None, if language: params["language"] = language - return client._get("/maps/api/geocode/json", params)["results"] + return client._request("/maps/api/geocode/json", params)["results"] diff --git a/googlemaps/geolocation.py b/googlemaps/geolocation.py new file mode 100644 index 00000000..9cbfb6b5 --- /dev/null +++ b/googlemaps/geolocation.py @@ -0,0 +1,105 @@ +# +# Copyright 2017 Google Inc. All rights reserved. +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +"""Performs requests to the Google Maps Geolocation API.""" +from googlemaps import exceptions + + +_GEOLOCATION_BASE_URL = "https://www.googleapis.com" + + +def _geolocation_extract(response): + """ + Mimics the exception handling logic in ``client._get_body``, but + for geolocation which uses a different response format. + """ + body = response.json() + if response.status_code in (200, 404): + return body + elif response.status_code == 403: + raise exceptions._RetriableRequest() + else: + try: + error = body["error"]["errors"][0]["reason"] + except KeyError: + error = None + raise exceptions.ApiError(response.status_code, error) + + +def geolocate(client, home_mobile_country_code=None, + home_mobile_network_code=None, radio_type=None, carrier=None, + consider_ip=None, cell_towers=None, wifi_access_points=None): + """ + The Google Maps Geolocation API returns a location and accuracy + radius based on information about cell towers and WiFi nodes given. + + See https://developers.google.com/maps/documentation/geolocation/intro + for more info, including more detail for each parameter below. + + :param home_mobile_country_code: The mobile country code (MCC) for + the device's home network. + :type home_mobile_country_code: string + + :param home_mobile_network_code: The mobile network code (MCC) for + the device's home network. + :type home_mobile_network_code: string + + :param radio_type: The mobile radio type. Supported values are + lte, gsm, cdma, and wcdma. While this field is optional, it + should be included if a value is available, for more accurate + results. + :type radio_type: string + + :param carrier: The carrier name. + :type carrier: string + + :param consider_ip: Specifies whether to fall back to IP geolocation + if wifi and cell tower signals are not available. Note that the + IP address in the request header may not be the IP of the device. + :type consider_ip: bool + + :param cell_towers: A list of cell tower dicts. See + https://developers.google.com/maps/documentation/geolocation/intro#cell_tower_object + for more detail. + :type cell_towers: list of dicts + + :param wifi_access_points: A list of WiFi access point dicts. See + https://developers.google.com/maps/documentation/geolocation/intro#wifi_access_point_object + for more detail. + :type wifi_access_points: list of dicts + """ + + params = {} + if home_mobile_country_code is not None: + params["homeMobileCountryCode"] = home_mobile_country_code + if home_mobile_network_code is not None: + params["homeMobileNetworkCode"] = home_mobile_network_code + if radio_type is not None: + params["radioType"] = radio_type + if carrier is not None: + params["carrier"] = carrier + if consider_ip is not None: + params["considerIp"] = consider_ip + if cell_towers is not None: + params["cellTowers"] = cell_towers + if wifi_access_points is not None: + params["wifiAccessPoints"] = wifi_access_points + + return client._request("/geolocation/v1/geolocate", {}, # No GET params + base_url=_GEOLOCATION_BASE_URL, + extract_body=_geolocation_extract, + post_json=params) diff --git a/googlemaps/places.py b/googlemaps/places.py index 38f2c389..a40f06c6 100644 --- a/googlemaps/places.py +++ b/googlemaps/places.py @@ -228,7 +228,7 @@ def _places(client, url_part, query=None, location=None, radius=None, params["pagetoken"] = page_token url = "/maps/api/place/%ssearch/json" % url_part - return client._get(url, params) + return client._request(url, params) def place(client, place_id, language=None): @@ -249,7 +249,7 @@ def place(client, place_id, language=None): params = {"placeid": place_id} if language: params["language"] = language - return client._get("/maps/api/place/details/json", params) + return client._request("/maps/api/place/details/json", params) def places_photo(client, photo_reference, max_width=None, max_height=None): @@ -291,7 +291,7 @@ def places_photo(client, photo_reference, max_width=None, max_height=None): # "extract_body" and "stream" args here are used to return an iterable # response containing the image file data, rather than converting from # json. - response = client._get("/maps/api/place/photo", params, + response = client._request("/maps/api/place/photo", params, extract_body=lambda response: response, requests_kwargs={"stream": True}) return response.iter_content() @@ -394,4 +394,4 @@ def _autocomplete(client, url_part, input_text, offset=None, location=None, params["components"] = convert.components(components) url = "/maps/api/place/%sautocomplete/json" % url_part - return client._get(url, params)["predictions"] + return client._request(url, params)["predictions"] diff --git a/googlemaps/roads.py b/googlemaps/roads.py index 1ab07534..120cc245 100644 --- a/googlemaps/roads.py +++ b/googlemaps/roads.py @@ -50,7 +50,7 @@ def snap_to_roads(client, path, interpolate=False): if interpolate: params["interpolate"] = "true" - return client._get("/v1/snapToRoads", params, + return client._request("/v1/snapToRoads", params, base_url=_ROADS_BASE_URL, accepts_clientid=False, extract_body=_roads_extract).get("snappedPoints", []) @@ -72,7 +72,7 @@ def nearest_roads(client, points): params = {"points": convert.location_list(points)} - return client._get("/v1/nearestRoads", params, + return client._request("/v1/nearestRoads", params, base_url=_ROADS_BASE_URL, accepts_clientid=False, extract_body=_roads_extract).get("snappedPoints", []) @@ -89,7 +89,7 @@ def speed_limits(client, place_ids): params = [("placeId", place_id) for place_id in convert.as_list(place_ids)] - return client._get("/v1/speedLimits", params, + return client._request("/v1/speedLimits", params, base_url=_ROADS_BASE_URL, accepts_clientid=False, extract_body=_roads_extract)["speedLimits"] @@ -110,7 +110,7 @@ def snapped_speed_limits(client, path): params = {"path": convert.location_list(path)} - return client._get("/v1/speedLimits", params, + return client._request("/v1/speedLimits", params, base_url=_ROADS_BASE_URL, accepts_clientid=False, extract_body=_roads_extract) diff --git a/googlemaps/timezone.py b/googlemaps/timezone.py index 339c5e6b..0b6370dc 100644 --- a/googlemaps/timezone.py +++ b/googlemaps/timezone.py @@ -51,4 +51,4 @@ def timezone(client, location, timestamp=None, language=None): if language: params["language"] = language - return client._get( "/maps/api/timezone/json", params) + return client._request( "/maps/api/timezone/json", params) diff --git a/test/test_geolocation.py b/test/test_geolocation.py new file mode 100644 index 00000000..8b6e1918 --- /dev/null +++ b/test/test_geolocation.py @@ -0,0 +1,44 @@ +# This Python file uses the following encoding: utf-8 +# +# Copyright 2017 Google Inc. All rights reserved. +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +"""Tests for the geocolocation module.""" + +import responses + +import test as _test +import googlemaps + +class GeolocationTest(_test.TestCase): + + def setUp(self): + self.key = 'AIzaasdf' + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_simple_geolocate(self): + responses.add(responses.POST, + 'https://www.googleapis.com/geolocation/v1/geolocate', + body='{"location": {"lat": 51.0,"lng": -0.1},"accuracy": 1200.4}', + status=200, + content_type='application/json') + + results = self.client.geolocate() + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual('https://www.googleapis.com/geolocation/v1/geolocate?' + 'key=%s' % self.key, responses.calls[0].request.url)