Commits

Sergiy Kuzmenko committed 90a481c Merge

added support for signed requests (biz API)

Comments (0)

Files changed (2)

geocode/google.py

+import hashlib
+import hmac
+import base64
+import urlparse
+
 from decimal import Decimal
 
 try:
 
 class GoogleGeocoderClient(object):
     
-    def __init__(self, sensor):
+    def __init__(self, sensor, client=None, key=None):
         """
         Sensor parameter is boolean, for details see:
         http://code.google.com/apis/maps/documentation/geocoding/
+
+        `client` and `key` parameters are for use by Google Maps API for
+        Business users. See
+        https://developers.google.com/maps/documentation/business/webservices
+        for more information.
+
         """
         self.sensor = sensor
+        self.client = client
+        self.key = key
+
+    def _build_request(self, output, address, latlng, bounds, region, language):
+        """ Helper function for building the request URL. """
+        
+        if (not (address or latlng)) or (address and latlng):
+            raise AssertionError("Either address or latlang is required (but not both)")
+        assert output in ("json", "xml")
+
+        tuple2str = lambda x: "%s,%s" % (x[0], x[1])
+
+        bool2str = lambda x:str(bool(x)).lower()
+
+        params = []
+
+        if address:
+            if isinstance(address, unicode):
+                params.append("address=" + quote_plus(address.encode("utf8")))
+            else:
+                params.append("address=" + quote_plus(address))
+
+        params.append("sensor=" + bool2str(self.sensor))
+
+        if latlng:
+            params.append("latlng=" + tuple2str(latlng))
+        if bounds:
+            params.append("bounds=" + "|".join(map(lambda x:tuple2str(x), bounds)))
+        if region:
+            params.append("region=" + quote_plus(region))
+        if language:
+            params.append("language=" + quote_plus(language))
+        if self.client:
+            params.append("client=" + self.client)
+
+        req = GOOGLE_GEOCODING_API_URL + output + "?" + "&".join(params)
+        
+        if not self.client or not self.key:
+            return req # unsigned request
+        
+        # using API for business, need to sign the request
+        url = urlparse.urlparse(req)
+
+        # We only need to sign the path+query part of the string
+        urlToSign = url.path + "?" + url.query
+
+        # Decode the private key into its binary format
+        decodedKey = base64.urlsafe_b64decode(self.key)
+
+        # Create a signature using the private key and the URL-encoded
+        # string using HMAC SHA1. This signature will be binary.
+        signature = hmac.new(decodedKey, urlToSign, hashlib.sha1)
+
+        # Encode the binary signature into base64 for use within a URL
+        encodedSignature = base64.urlsafe_b64encode(signature.digest())
+        originalUrl = url.scheme + "://" + url.netloc + url.path + "?" + url.query
+        return originalUrl + "&signature=" + encodedSignature
     
     def geocode_raw(self, output="json", address=None, latlng=None, bounds=None, region=None, language=None):
         """ Returns raw geocoded address information in json or xml format.
         
         Either address or latlng (but not both) parameters are required. the rest is optional.
         """
-        if (not (address or latlng)) or (address and latlng):
-            raise AssertionError("Either address or latlang is required (but not both)")
-        assert output in ("json", "xml")
-        
-        tuple2str = lambda x: "%s,%s" % (x[0], x[1])
-        
-        bool2str = lambda x:str(bool(x)).lower()
-        
-        params = ["sensor=" + bool2str(self.sensor)]
-        
-        if address:
-            if isinstance(address, unicode):
-                params.append("address=" + quote_plus(address.encode("utf8")))
-            else:
-                params.append("address=" + quote_plus(address))
-        if latlng:
-            params.append("latlng=" + tuple2str(latlng))
-        if bounds:
-            params.append("bounds=" + "|".join(map(lambda x:tuple2str(x), bounds)))
-        if region:
-            params.append("region=" + quote_plus(region))
-        if language:
-            params.append("language=" + quote_plus(language))
-        
-        url = GOOGLE_GEOCODING_API_URL + output + "?" + "&".join(params)
-        
+        url = self._build_request(output, address, latlng, bounds, region, language)
         handler = urlopen(url)
         return handler.read()
     
 
 import unittest
 from decimal import Decimal
-from geocode.google import GoogleGeocoder, GoogleGeocoderClient
+from geocode.google import GoogleGeocoder, GoogleGeocoderClient, GOOGLE_GEOCODING_API_URL
+from urlparse import urlparse
 
 class GoogleGeocoderValidAddressTests(unittest.TestCase):
     """
         self.assertTrue(lng == expected_lng, "Expecting longitude `%s` but got `%s`" % (expected_lng, lng))
 
 
+class GoogleGeocoderClientTests(unittest.TestCase):
+    """ Test GoogleGRocoderClient """
+    
+    def _get_params(self, url):
+        # parses url and returns query params as a dictionary
+        parsed_url = urlparse(url)
+        return dict(map(lambda x:x.split("=", 1), parsed_url.query.split("&")))
+        
+    
+    def test_signed_request(self):
+        """ Test that signing is performed correctly when using Google Maps API
+        for Business
+        """
+        # based on the example from
+        # https://developers.google.com/maps/documentation/business/webservices#signature_examples
+        
+        CLIENT_ID = 'clientID'
+        
+        client = GoogleGeocoderClient(
+            False, client=CLIENT_ID, key='vNIXE0xscrmjlyV-12Nj_BvUPaw='
+        )
+        url = client._build_request("json", "New York", None, None, None, None)
+        params = self._get_params(url)
+        
+        self.assertIn('client', params)
+        self.assertIn('signature', params)
+        self.assertEqual(params['client'], CLIENT_ID)
+        self.assertEqual(params['signature'], 'KrU1TzVQM7Ur0i8i7K3huiw3MsA=')
+    
+    def test_unsigned_request(self):
+        """ Test that request is not signed when NOT using Google Maps API
+        for Business
+        """
+        
+        client = GoogleGeocoderClient(False)
+        url = client._build_request("json", "New York", None, None, None, None)
+        params = self._get_params(url)
+        
+        self.assertNotIn('client', params)
+        self.assertNotIn('signature', params)
+    
+
 if __name__ == '__main__':
     unittest.main()