Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/27acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: 2.7 Acceptance CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
runs-on: ubuntu-latest
container:
image: python:2.7.18-buster

steps:
- uses: actions/checkout@v4
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install mock unittest2

- name: Run Tests
run: |
python -m test
31 changes: 31 additions & 0 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Acceptance CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install mock

- name: Run Tests
run: |
python -m unittest test.py
31 changes: 31 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Upload Python Package

on:
release:
types: [created]

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME_PUBLIC }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD_PUBLIC }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.. _geocoder_api:

.. image:: https://travis-ci.org/imtapps/ggeocoder.svg?branch=master
:target: https://travis-ci.org/imtapps/ggeocoder

*******************
Google Geocoder API
*******************
Expand Down
106 changes: 55 additions & 51 deletions ggeocoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,63 @@
import base64
import hashlib
import hmac
import urllib
import urllib2
import urlparse
import sys

if sys.version_info > (3, 0):
from urllib.parse import urlparse, urlencode
from urllib.request import urlopen
else:
from urllib import urlencode
from urllib2 import urlopen
from urlparse import urlparse

try:
import json
except ImportError:
import simplejson as json


VERSION = '1.0.2'
VERSION = '2.0.0'

__all__ = ['Geocoder', 'GeocoderResult', 'GeoResult', 'GeocodeError']

__all__ = ['Geocoder', 'GeocoderResult', 'GeoResult', 'GeocodeError',]

class GeocodeError(Exception):
"""
Base class for errors in the :mod:`ggeocoder` module.

Methods of the :class:`Geocoder` raise this when the Google Maps API
returns a status of anything other than 'OK'.
See http://code.google.com/apis/maps/documentation/geocoding/index.html#StatusCodes

See http://code.google.com/apis/maps/documentation/geocoding/index.html#StatusCodes # noqa: E501
for status codes and their meanings.
"""
G_GEO_OK = "OK"
G_GEO_ZERO_RESULTS = "ZERO_RESULTS"
G_GEO_OVER_QUERY_LIMIT = "OVER_QUERY_LIMIT"
G_GEO_REQUEST_DENIED = "REQUEST_DENIED"
G_GEO_MISSING_QUERY = "INVALID_REQUEST"

def __init__(self, status, url=None, response=None):
"""Create an exception with a status and optional full response.

:param status: Either a ``G_GEO_`` code or a string explaining the
exception.
:type status: int or string
:param url: The query URL that resulted in the error, if any.
:type url: string
:param response: The actual response returned from Google, if any.
:type response: dict

"""
super(GeocodeError, self).__init__(status)
self.status = status
self.url = url
self.response = response

def __str__(self):
"""Return a string representation of this :exc:`GeocoderError`."""
return 'Error: {0}\nQuery: {1}'.format(self.status, self.url)

def __unicode__(self, *args, **kwargs):
"""Return a unicode representation of this :exc:`GeocoderError`."""
return unicode(self.__str__())


class GeoResult(object):
"""
Expand All @@ -84,11 +87,8 @@ def __eq__(self, other):
return False

def __str__(self):
return unicode(self).encode('utf-8')

def __unicode__(self):
return self.formatted_address

def __getattr__(self, name):
attr, prop = self.get_property_components(name)

Expand Down Expand Up @@ -142,7 +142,6 @@ def raw(self):
return self.data



class GeocoderResult(object):
"""
Helps process all the results returned from Google Maps API
Expand All @@ -156,7 +155,6 @@ class GeocoderResult(object):
print result.formatted_address, result.coordinates

You can also customize the result_class created if you'd like.

"""

def __init__(self, data, result_class):
Expand All @@ -172,17 +170,17 @@ def __getitem__(self, idx):
class Geocoder(object):
"""
Interface for interacting with Google's Geocoding V3's API.
http://code.google.com/apis/maps/documentation/geocoding/
https://developers.google.com/maps/documentation/geocoding/start?csw=1

If you have a Google Maps Premier account, you can supply your
client_id and private_key and the :class:`Geocoder` will make
the request with a properly signed url
"""
PREMIER_CREDENTIALS_ERROR = "You must provide both a client_id and private_key to use Premier Account."
GOOGLE_API_URL = 'http://maps.googleapis.com/maps/api/geocode/json?'
PREMIER_CREDENTIALS_ERROR = "You must provide both a client_id and private_key to use Premier Account." # noqa: E501
GOOGLE_API_URL = 'https://maps.googleapis.com/maps/api/geocode/json?'
TIMEOUT_SECONDS = 3

def __init__(self, client_id=None, private_key=None):
def __init__(self, client_id=None, private_key=None, api_key=None):
"""
Google Maps API Premier users can provide credentials to make 100,000
requests a day vs the standard 2,500 requests a day without
Expand All @@ -194,23 +192,27 @@ def __init__(self, client_id=None, private_key=None):
:type client_id: str
:param private_key: private key used to sign urls
:type private_key: str

:param api_key: Google Maps API key.
this will trump if it is present. Google has moved away from the
"premier accounts" and now just requires an API key
:type api_key: str
"""
self.credentials = (client_id, private_key)
self.api_key = api_key
if any(self.credentials) and not all(self.credentials):
raise GeocodeError(self.PREMIER_CREDENTIALS_ERROR)

def geocode(self, address, **params):
"""
| Params may be any valid parameter accepted by Google's API.
| http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests
| http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests # noqa: E501
"""
return self._get_geocoder_result(address=address, **params)

def reverse_geocode(self, lat, lng, **params):
"""
| Params may be any valid parameter accepted by Google's API.
| http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests
| http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests # noqa: E501
"""
return self._get_geocoder_result(latlng="%s,%s" % (lat, lng), **params)

Expand All @@ -219,13 +221,11 @@ def use_premier_key(self):
return all(self.credentials)

def _get_geocoder_result(self, result_class=GeoResult, **params):
geo_params = {'sensor': 'false'} # API says sensor must always have a value
geo_params.update(params)
return GeocoderResult(self._get_results(params=geo_params), result_class)
return GeocoderResult(self._get_results(params=params), result_class)

def _get_results(self, params=None):
url = self._get_request_url(params or {})
response = urllib2.urlopen(url, timeout=self.TIMEOUT_SECONDS)
response = urlopen(url, timeout=self.TIMEOUT_SECONDS)
return self._process_response(response, url)

def _process_response(self, response, url):
Expand All @@ -235,9 +235,11 @@ def _process_response(self, response, url):
return j['results']

def _get_request_url(self, params):
encoded_params = urllib.urlencode(params)
encoded_params = urlencode(params)
url = self.GOOGLE_API_URL + encoded_params
if self.use_premier_key:
if self.api_key:
url = url + "&key={}".format(self.api_key)
elif self.use_premier_key:
url = self._get_premier_url(url)
return url

Expand All @@ -252,52 +254,54 @@ def _generate_signature(self, base_url, private_key):
"""
http://code.google.com/apis/maps/documentation/webservices/index.html#PythonSignatureExample
"""
url = urlparse.urlparse(base_url)
url = urlparse(base_url)
url_to_sign = url.path + '?' + url.query
decoded_key = base64.urlsafe_b64decode(private_key)
signature = hmac.new(decoded_key, url_to_sign, hashlib.sha1)
return base64.urlsafe_b64encode(signature.digest())
signature = hmac.new(decoded_key, url_to_sign.encode(), hashlib.sha1)
return base64.urlsafe_b64encode(signature.digest()).decode('utf-8')


if __name__ == "__main__":
import sys
from optparse import OptionParser

def main():
"""
Geocodes a location given on the command line.

Usage:
ggeocoder.py "1600 amphitheatre mountain view ca" [YOUR_API_KEY]
ggeocoder.py 37.4218272,-122.0842409 [YOUR_API_KEY]

When providing a latitude and longitude on the command line, ensure
they are separated by a comma and no space.

"""
usage = "usage: %prog [options] address"
parser = OptionParser(usage, version=VERSION)
parser.add_option("-c", "--client_id",
dest="client_id", help="Your Google Maps Client Id key")
dest="client_id",
help="Your Google Maps Client Id key")
parser.add_option("-k", "--private_key",
dest="private_key", help="Your Google Maps Private Signature key")
dest="private_key",
help="Your Google Maps Private Signature key")
(options, args) = parser.parse_args()

if len(args) != 1:
parser.print_usage()
sys.exit(1)

query = args[0]
gcoder = Geocoder(options.client_id, options.private_key)

try:
result = gcoder.geocode(query)
except GeocodeError, err:
except GeocodeError as err:
sys.stderr.write('%s\n%s\nResponse:\n' % (err.url, err))
json.dump(err.response, sys.stderr, indent=4)
sys.exit(1)

for r in result:
print r
print r.coordinates
print(r)
print(r.coordinates)
main()
12 changes: 8 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


import os
from distutils.core import setup
from setuptools import setup

import ggeocoder

Expand All @@ -12,7 +12,7 @@
author='Aaron Madison',
url='https://github.com/imtapps/ggeocoder',
description='A Python library for working with Google Geocoding API V3.',
long_description=file(
long_description=open(
os.path.join(os.path.dirname(__file__), 'README.rst')
).read(),
py_modules=['ggeocoder'],
Expand All @@ -23,7 +23,11 @@
'Intended Audience :: Developers',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'License :: OSI Approved :: BSD License',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
Expand All @@ -32,4 +36,4 @@
],
keywords='google maps api v3 geocode geocoding geocoder reverse',
license='BSD License',
)
)
Loading