Skip to content

Commit

Permalink
Support for rate limiting. Closes googlemaps#75.
Browse files Browse the repository at this point in the history
Change-Id: I5c8c716b8f0b780448dc3efcb59f6a125ec1f368
  • Loading branch information
stephenmcd committed Sep 15, 2015
1 parent ed3568e commit 0f345ca
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 9 deletions.
44 changes: 35 additions & 9 deletions googlemaps/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"""

import base64
import collections
from datetime import datetime
from datetime import timedelta
import hashlib
Expand All @@ -47,12 +48,20 @@ class Client(object):

def __init__(self, key=None, client_id=None, client_secret=None,
timeout=None, connect_timeout=None, read_timeout=None,
retry_timeout=60, requests_kwargs=None):
retry_timeout=60, requests_kwargs=None,
queries_per_second=None):
"""
:param key: Maps API key. Required, unless "client_id" and
"client_secret" are set.
:type key: string
:param client_id: (for Maps API for Work customers) Your client ID.
:type client_id: string
:param client_secret: (for Maps API for Work customers) Your client
secret (base64 encoded).
:type client_secret: string
:param timeout: Combined connect and read timeout for HTTP requests, in
seconds. Specify "None" for no timeout.
:type timeout: int
Expand All @@ -71,12 +80,12 @@ def __init__(self, key=None, client_id=None, client_secret=None,
seconds.
:type retry_timeout: int
:param client_id: (for Maps API for Work customers) Your client ID.
:type client_id: string
:param client_secret: (for Maps API for Work customers) Your client
secret (base64 encoded).
:type client_secret: string
:param queries_per_second: Manually specify the number of queries
per second permitted, namely 10 for free clients or larger
for Maps for Work. If the rate limit is reach, the client will
wait the appropriate amount of time before it runs the current
query.
:type queries_per_second: int
:raises ValueError: when either credentials are missing, incomplete
or invalid.
Expand Down Expand Up @@ -123,6 +132,11 @@ def __init__(self, key=None, client_id=None, client_secret=None,
"verify": True, # NOTE(cbro): verify SSL certs.
})

# If `queries_per_second` isn't provided, default maxlen to 0.
# This ensures `sent_times` evals to False when we check whether
# to rate limit.
self.sent_times = collections.deque("", queries_per_second or 0)

def _get(self, url, params, first_request_time=None, retry_counter=0,
base_url=_DEFAULT_BASE_URL, accepts_clientid=True, extract_body=None):
"""Performs HTTP GET request with credentials, returning the body as
Expand Down Expand Up @@ -184,10 +198,22 @@ def _get(self, url, params, first_request_time=None, retry_counter=0,
return self._get(url, params, first_request_time, retry_counter + 1,
base_url, accepts_clientid, extract_body)

# If `queries_per_second` was configured and at least that many
# queries have been made, check if the time of the earliest query
# is under a second ago - if so, sleep for the difference between
# it, and a second.
if self.sent_times and len(self.sent_times) == self.sent_times.maxlen:
elapsed_since_earliest = time.time() - self.sent_times[0]
if elapsed_since_earliest < 1:
time.sleep(1 - elapsed_since_earliest)

try:
if extract_body:
return extract_body(resp)
return self._get_body(resp)
result = extract_body(resp)
else:
result = self._get_body(resp)
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,
Expand Down
23 changes: 23 additions & 0 deletions test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"""Tests for client module."""

import responses
import time

import googlemaps
from googlemaps import client as _client
Expand All @@ -41,6 +42,28 @@ def test_urlencode(self):
encoded_params = _client.urlencode_params([("address", "=Sydney ~")])
self.assertEqual("address=%3DSydney+~", encoded_params)

@responses.activate
def test_queries_per_second(self):
# This test assumes that the time to run a mocked query is
# relatively small, eg a few milliseconds. We define a rate of
# 3 queries per second, and run double that, which should take at
# least 1 second but no more than 2.
queries_per_second = 3
query_range = range(queries_per_second * 2)
for _ in query_range:
responses.add(responses.GET,
"https://maps.googleapis.com/maps/api/geocode/json",
body='{"status":"OK","results":[]}',
status=200,
content_type="application/json")
client = googlemaps.Client(key="AIzaasdf",
queries_per_second=queries_per_second)
start = time.time()
for _ in query_range:
client.geocode("Sesame St.")
end = time.time()
self.assertTrue(start + 1 < end < start + 2)

@responses.activate
def test_key_sent(self):
responses.add(responses.GET,
Expand Down

0 comments on commit 0f345ca

Please sign in to comment.