diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..e95e611e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# 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. + +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +.github/ @googlemaps/admin diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..36f9436b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# 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. + +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/scripts/distribution.sh b/.github/scripts/distribution.sh index f7c08690..e779cd1d 100755 --- a/.github/scripts/distribution.sh +++ b/.github/scripts/distribution.sh @@ -1,4 +1,18 @@ #!/bin/bash +# Copyright 2022 Google LLC +# +# 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. + rm -rf dist diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 5cd76712..39e7f9f0 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -1,4 +1,18 @@ #!/bin/bash +# Copyright 2022 Google LLC +# +# 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. + set -exo pipefail @@ -6,8 +20,9 @@ if ! python3 -m pip --version; then curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py sudo python3 get-pip.py sudo python3 -m pip install --upgrade setuptools - sudo python3 -m pip install nox + sudo python3 -m pip install nox twine else sudo python3 -m pip install --upgrade setuptools python3 -m pip install nox + python3 -m pip install --prefer-binary twine fi diff --git a/.github/stale.yml b/.github/stale.yml index 8ed0e080..1d39e65d 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,3 +1,17 @@ +# Copyright 2022 Google LLC +# +# 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. + # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 00000000..a7b2d39c --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,44 @@ +# Copyright 2022 Google LLC +# +# 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. + +# https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings + +rebaseMergeAllowed: true +squashMergeAllowed: true +mergeCommitAllowed: false +deleteBranchOnMerge: true +branchProtectionRules: +- pattern: main + isAdminEnforced: false + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + - 'cla/google' + - 'test' + - 'snippet-bot check' + - 'header-check' + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true +- pattern: master + isAdminEnforced: false + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + - 'cla/google' + - 'test' + - 'snippet-bot check' + - 'header-check' + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true +permissionRules: + - team: admin + permission: admin diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 00000000..6b46ebbb --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,32 @@ +# Copyright 2022 Google LLC +# +# 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. + +name: Dependabot +on: pull_request + +permissions: + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.SYNCED_GITHUB_TOKEN_REPO}} + steps: + - name: approve + run: gh pr review --approve "$PR_URL" + - name: merge + run: gh pr merge --auto --squash --delete-branch "$PR_URL" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..c83d6c38 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,63 @@ +# Copyright 2023 Google LLC +# +# 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. + +# A workflow that pushes artifacts to Sonatype +name: Publish + +on: + push: + tags: + - '*' + repository_dispatch: + types: [docs] + +jobs: + docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Python + uses: "actions/setup-python@v3" + with: + python-version: "3.9" + + - name: Install dependencies + run: ./.github/scripts/install.sh + + - name: Generate docs + run: python3 -m nox --session docs + + - name: Update gh-pages branch with docs + run: | + echo "Creating tar for generated docs" + cd ./docs/_build/html && tar cvf ~/docs.tar . + + echo "Unpacking tar into gh-pages branch" + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + cd $GITHUB_WORKSPACE && git checkout gh-pages && tar xvf ~/docs.tar + + - name: PR Changes + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} + commit-message: 'docs: Update docs' + committer: googlemaps-bot + author: googlemaps-bot + title: 'docs: Update docs' + body: | + Updated GitHub pages with latest from `python3 -m nox --session docs`. + branch: googlemaps-bot/update_gh_pages diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index a0ea83af..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2020 Google LLC -# -# 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. - -# A workflow that pushes artifacts to Sonatype -name: Publish - -on: - push: - tags: - - '*' - repository_dispatch: - types: [publish] - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Python - uses: "actions/setup-python@v1" - with: - python-version: "3.6" - - - name: Install dependencies - run: ./.github/scripts/install.sh - - - name: Run distribution - run: python3 -m nox -e distribution - - - name: Deploy to PyPi - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bebedf46..a0a28dce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,19 +19,35 @@ on: jobs: release: runs-on: ubuntu-latest + env: + PYTHONDONTWRITEBYTECODE: 1 steps: + - name: Setup Python + uses: "actions/setup-python@v3" + with: + python-version: "3.9" - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} + - name: Install dependencies + run: ./.github/scripts/install.sh + - name: Run distribution + run: python3 -m nox -e distribution + - name: Cleanup old dist + run: rm -rf googlemaps-* dist/ - name: Semantic Release - uses: cycjimmy/semantic-release-action@v2 + uses: cycjimmy/semantic-release-action@v3 with: + semantic_version: 19 extra_plugins: | "@semantic-release/commit-analyzer" "@semantic-release/release-notes-generator" "@google/semantic-release-replace-plugin" - "@semantic-release/git - "@semantic-release/github + "@semantic-release/exec" + "@semantic-release/git" + "@semantic-release/github" env: GH_TOKEN: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58839686..570a087a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,18 +24,18 @@ on: branches: ['*'] jobs: - test: + matrix: name: "Run tests on Python ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Python - uses: "actions/setup-python@v1" + uses: "actions/setup-python@v3" with: python-version: "${{ matrix.python-version }}" @@ -46,3 +46,11 @@ jobs: run: | python3 -m nox --session "tests-${{ matrix.python-version }}" python3 -m nox -e distribution + test: + name: Wait for matrix to finish + needs: [matrix] + runs-on: ubuntu-latest + steps: + - run: | + echo "Test matrix finished"; + exit 0; diff --git a/.gitignore b/.gitignore index c4477ba5..93e7a2fc 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ googlemaps.egg-info *.egg .vscode/ .idea/ +index.py diff --git a/.releaserc b/.releaserc index 981e4031..5f53e6f5 100644 --- a/.releaserc +++ b/.releaserc @@ -11,8 +11,9 @@ plugins: to: "__version__ = \"${nextRelease.version}\"" - files: - "./setup.py" - from: "version=\".*\"" - to: "version=\"${nextRelease.version}\"" + from: 'version=".*"' + to: 'version="${nextRelease.version}"' + - [ "@semantic-release/exec", { publishCmd: "python3 setup.py sdist && python3 -m twine upload dist/*" }] - - "@semantic-release/git" - assets: - "./googlemaps/__init__.py" diff --git a/README.md b/README.md index 31840082..40823790 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ Python Client for Google Maps Services ==================================== -[![Build Status](https://travis-ci.org/googlemaps/google-maps-services-python.svg?branch=master)](https://travis-ci.org/googlemaps/google-maps-services-python) +![Test](https://github.com/googlemaps/google-maps-services-js/workflows/Test/badge.svg) +![Release](https://github.com/googlemaps/google-maps-services-js/workflows/Release/badge.svg) [![codecov](https://codecov.io/gh/googlemaps/google-maps-services-python/branch/master/graph/badge.svg)](https://codecov.io/gh/googlemaps/google-maps-services-python) [![PyPI version](https://badge.fury.io/py/googlemaps.svg)](https://badge.fury.io/py/googlemaps) ![PyPI - Downloads](https://img.shields.io/pypi/dd/googlemaps) @@ -25,6 +26,7 @@ APIs: - Roads API - Places API - Maps Static API + - Address Validation API Keep in mind that the same [terms and conditions](https://developers.google.com/maps/terms) apply to usage of the APIs when they're accessed through this library. @@ -84,6 +86,16 @@ directions_result = gmaps.directions("Sydney Town Hall", "Parramatta, NSW", mode="transit", departure_time=now) + +# Validate an address with address validation +addressvalidation_result = gmaps.addressvalidation(['1600 Amphitheatre Pk'], + regionCode='US', + locality='Mountain View', + enableUspsCass=True) + +# Get an Address Descriptor of a location in the reverse geocoding response +address_descriptor_result = gmaps.reverse_geocode((40.714224, -73.961452), enable_address_descriptor=True) + ``` For more usage examples, check out [the tests](https://github.com/googlemaps/google-maps-services-python/tree/master/tests). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..6d19135d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Report a security issue + +To report a security issue, please use https://g.co/vulnz. We use +https://g.co/vulnz for our intake, and do coordination and disclosure here on +GitHub (including using GitHub Security Advisory). The Google Security Team will +respond within 5 working days of your report on g.co/vulnz. + +To contact us about other bugs, please open an issue on GitHub. + +> **Note**: This file is synchronized from the https://github.com/googlemaps/.github repository. diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 00000000..1c38ca3d --- /dev/null +++ b/coverage.xml @@ -0,0 +1,849 @@ + + + + + + /Users/anglarett/Public/Drop Box/dev-se-git/google-maps-services-python + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/conf.py b/docs/conf.py index 19cdfef5..0d8314fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,17 @@ +# Copyright 2022 Google LLC +# +# 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. + # -*- coding: utf-8 -*- # # Maps API documentation build configuration file, created by diff --git a/googlemaps/__init__.py b/googlemaps/__init__.py index b6f45f8c..61ec45d0 100644 --- a/googlemaps/__init__.py +++ b/googlemaps/__init__.py @@ -15,7 +15,7 @@ # the License. # -__version__ = "4.4.2" +__version__ = "4.10.0" from googlemaps.client import Client from googlemaps import exceptions diff --git a/googlemaps/addressvalidation.py b/googlemaps/addressvalidation.py new file mode 100644 index 00000000..149f3b48 --- /dev/null +++ b/googlemaps/addressvalidation.py @@ -0,0 +1,81 @@ +# +# Copyright 2022 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 Address Validation API.""" +from googlemaps import exceptions + + +_ADDRESSVALIDATION_BASE_URL = "https://addressvalidation.googleapis.com" + + +def _addressvalidation_extract(response): + """ + Mimics the exception handling logic in ``client._get_body``, but + for addressvalidation which uses a different response format. + """ + body = response.json() + return body + + # if response.status_code in (200, 404): + # return body + + # try: + # error = body["error"]["errors"][0]["reason"] + # except KeyError: + # error = None + + # if response.status_code == 403: + # raise exceptions._OverQueryLimit(response.status_code, error) + # else: + # raise exceptions.ApiError(response.status_code, error) + + +def addressvalidation(client, addressLines, regionCode=None , locality=None, enableUspsCass=None): + """ + The Google Maps Address Validation API returns a verification of an address + See https://developers.google.com/maps/documentation/address-validation/overview + request must include parameters below. + :param addressLines: The address to validate + :type addressLines: array + :param regionCode: (optional) The country code + :type regionCode: string + :param locality: (optional) Restrict to a locality, ie:Mountain View + :type locality: string + :param enableUspsCass For the "US" and "PR" regions only, you can optionally enable the Coding Accuracy Support System (CASS) from the United States Postal Service (USPS) + :type locality: boolean + """ + + params = { + "address":{ + "addressLines": addressLines + } + } + + if regionCode is not None: + params["address"]["regionCode"] = regionCode + + if locality is not None: + params["address"]["locality"] = locality + + if enableUspsCass is not False or enableUspsCass is not None: + params["enableUspsCass"] = enableUspsCass + + return client._request("/v1:validateAddress", {}, # No GET params + base_url=_ADDRESSVALIDATION_BASE_URL, + extract_body=_addressvalidation_extract, + post_json=params) + \ No newline at end of file diff --git a/googlemaps/client.py b/googlemaps/client.py index 546f7796..d1f4ab6a 100644 --- a/googlemaps/client.py +++ b/googlemaps/client.py @@ -22,6 +22,7 @@ import base64 import collections +import logging from datetime import datetime from datetime import timedelta import functools @@ -31,6 +32,8 @@ import requests import random import time +import math +import sys import googlemaps @@ -39,21 +42,24 @@ except ImportError: # Python 2 from urllib import urlencode +logger = logging.getLogger(__name__) + _X_GOOG_MAPS_EXPERIENCE_ID = "X-Goog-Maps-Experience-ID" _USER_AGENT = "GoogleGeoApiClientPython/%s" % googlemaps.__version__ _DEFAULT_BASE_URL = "https://maps.googleapis.com" -_RETRIABLE_STATUSES = set([500, 503, 504]) +_RETRIABLE_STATUSES = {500, 503, 504} -class Client(object): +class Client: """Performs requests to the Google Maps API web services.""" 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, - queries_per_second=50, channel=None, + queries_per_second=60, queries_per_minute=6000,channel=None, retry_over_query_limit=True, experience_id=None, + requests_session=None, base_url=_DEFAULT_BASE_URL): """ :param key: Maps API key. Required, unless "client_id" and @@ -92,11 +98,16 @@ def __init__(self, key=None, client_id=None, client_secret=None, seconds. :type retry_timeout: int - :param queries_per_second: Number of queries per second permitted. + :param queries_per_second: Number of queries per second permitted. Unset queries_per_minute to None. If set smaller number will be used. If the rate limit is reached, the client will sleep for the appropriate amount of time before it runs the current query. :type queries_per_second: int + :param queries_per_minute: Number of queries per minute permitted. Unset queries_per_second to None. If set smaller number will be used. + If the rate limit is reached, the client will sleep for the + appropriate amount of time before it runs the current query. + :type queries_per_minute: int + :param retry_over_query_limit: If True, requests that result in a response indicating the query rate limit was exceeded will be retried. Defaults to True. @@ -116,6 +127,9 @@ def __init__(self, key=None, client_id=None, client_secret=None, implemented. See the official requests docs for more info: http://docs.python-requests.org/en/latest/api/#main-interface :type requests_kwargs: dict + + :param requests_session: Reused persistent session for flexibility. + :type requests_session: requests.Session :param base_url: The base URL for all requests. Defaults to the Maps API server. Should not have a trailing slash. @@ -130,15 +144,13 @@ def __init__(self, key=None, client_id=None, client_secret=None, raise ValueError("Invalid API key provided.") if channel: - if not client_id: - raise ValueError("The channel argument must be used with a " - "client ID") if not re.match("^[a-zA-Z0-9._-]*$", channel): raise ValueError("The channel argument must be an ASCII " "alphanumeric string. The period (.), underscore (_)" - "and hyphen (-) characters are allowed.") + "and hyphen (-) characters are allowed. If used without " + "client_id, it must be 0-999.") - self.session = requests.Session() + self.session = requests_session or requests.Session() self.key = key if timeout and (connect_timeout or read_timeout): @@ -167,10 +179,25 @@ def __init__(self, key=None, client_id=None, client_secret=None, "timeout": self.timeout, "verify": True, # NOTE(cbro): verify SSL certs. }) - + self.queries_per_second = queries_per_second + self.queries_per_minute = queries_per_minute + try: + if (type(self.queries_per_second) == int and type(self.queries_per_minute) == int ): + self.queries_quota = math.floor(min(self.queries_per_second, self.queries_per_minute/60)) + elif (self.queries_per_second and type(self.queries_per_second) == int ): + self.queries_quota = math.floor(self.queries_per_second) + elif (self.queries_per_minute and type(self.queries_per_minute) == int ): + self.queries_quota = math.floor(self.queries_per_minute/60) + else: + sys.exit("MISSING VALID NUMBER for queries_per_second or queries_per_minute") + logger.info("API queries_quota: %s", self.queries_quota) + + except NameError: + sys.exit("MISSING VALUE for queries_per_second or queries_per_minute") + self.retry_over_query_limit = retry_over_query_limit - self.sent_times = collections.deque("", queries_per_second) + self.sent_times = collections.deque("", self.queries_quota) self.set_experience_id(experience_id) self.base_url = base_url @@ -301,7 +328,7 @@ def _request(self, url, params, first_request_time=None, retry_counter=0, # 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: + if self.sent_times and len(self.sent_times) == self.queries_quota: elapsed_since_earliest = time.time() - self.sent_times[0] if elapsed_since_earliest < 1: time.sleep(1 - elapsed_since_earliest) @@ -400,7 +427,7 @@ def _generate_auth_url(self, path, params, accepts_clientid): from googlemaps.places import places_autocomplete from googlemaps.places import places_autocomplete_query from googlemaps.maps import static_map - +from googlemaps.addressvalidation import addressvalidation def make_api_method(func): """ @@ -444,6 +471,7 @@ def wrapper(*args, **kwargs): Client.places_autocomplete = make_api_method(places_autocomplete) Client.places_autocomplete_query = make_api_method(places_autocomplete_query) Client.static_map = make_api_method(static_map) +Client.addressvalidation = make_api_method(addressvalidation) def sign_hmac(secret, payload): diff --git a/googlemaps/convert.py b/googlemaps/convert.py index 7dfa9882..2b3d056e 100644 --- a/googlemaps/convert.py +++ b/googlemaps/convert.py @@ -160,9 +160,7 @@ def _is_list(arg): return False if isinstance(arg, str): # Python 3-only, as str has __iter__ return False - return (not _has_method(arg, "strip") - and _has_method(arg, "__getitem__") - or _has_method(arg, "__iter__")) + return _has_method(arg, "__getitem__") if not _has_method(arg, "strip") else _has_method(arg, "__iter__") def is_string(val): diff --git a/googlemaps/directions.py b/googlemaps/directions.py index b38a4cdf..353145cc 100644 --- a/googlemaps/directions.py +++ b/googlemaps/directions.py @@ -33,7 +33,7 @@ def directions(client, origin, destination, :param destination: The address or latitude/longitude value from which you wish to calculate directions. You can use a place_id as destination - by putting 'place_id:' as a preffix in the passing parameter. + by putting 'place_id:' as a prefix in the passing parameter. :type destination: string, dict, list, or tuple :param mode: Specifies the mode of transport to use when calculating diff --git a/googlemaps/distance_matrix.py b/googlemaps/distance_matrix.py index 1d848253..a30cbe09 100755 --- a/googlemaps/distance_matrix.py +++ b/googlemaps/distance_matrix.py @@ -26,17 +26,19 @@ def distance_matrix(client, origins, destinations, transit_routing_preference=None, traffic_model=None, region=None): """ Gets travel distance and time for a matrix of origins and destinations. - :param origins: One or more locations and/or latitude/longitude values, - from which to calculate distance and time. If you pass an address as - a string, the service will geocode the string and convert it to a + :param origins: One or more addresses, Place IDs, and/or latitude/longitude + values, from which to calculate distance and time. Each Place ID string + must be prepended with 'place_id:'. If you pass an address as a string, + the service will geocode the string and convert it to a latitude/longitude coordinate to calculate directions. :type origins: a single location, or a list of locations, where a location is a string, dict, list, or tuple - :param destinations: One or more addresses and/or lat/lng values, to - which to calculate distance and time. If you pass an address as a - string, the service will geocode the string and convert it to a - latitude/longitude coordinate to calculate directions. + :param destinations: One or more addresses, Place IDs, and/or lat/lng values + , to which to calculate distance and time. Each Place ID string must be + prepended with 'place_id:'. If you pass an address as a string, the + service will geocode the string and convert it to a latitude/longitude + coordinate to calculate directions. :type destinations: a single location, or a list of locations, where a location is a string, dict, list, or tuple diff --git a/googlemaps/geocoding.py b/googlemaps/geocoding.py index b665d776..590bb627 100644 --- a/googlemaps/geocoding.py +++ b/googlemaps/geocoding.py @@ -19,7 +19,7 @@ from googlemaps import convert -def geocode(client, address=None, components=None, bounds=None, region=None, +def geocode(client, address=None, place_id=None, components=None, bounds=None, region=None, language=None): """ Geocoding is the process of converting addresses @@ -30,6 +30,10 @@ def geocode(client, address=None, components=None, bounds=None, region=None, :param address: The address to geocode. :type address: string + :param place_id: A textual identifier that uniquely identifies a place, + returned from a Places search. + :type place_id: string + :param components: A component filter for which you wish to obtain a geocode, for example: ``{'administrative_area': 'TX','country': 'US'}`` :type components: dict @@ -45,7 +49,9 @@ def geocode(client, address=None, components=None, bounds=None, region=None, :param language: The language in which to return results. :type language: string - :rtype: list of geocoding results. + :rtype: result dict with the following keys: + status: status code + results: list of geocoding results """ params = {} @@ -53,6 +59,9 @@ def geocode(client, address=None, components=None, bounds=None, region=None, if address: params["address"] = address + if place_id: + params["place_id"] = place_id + if components: params["components"] = convert.components(components) @@ -65,11 +74,11 @@ def geocode(client, address=None, components=None, bounds=None, region=None, if language: params["language"] = language - return client._request("/maps/api/geocode/json", params).get("results", []) + return client._request("/maps/api/geocode/json", params) def reverse_geocode(client, latlng, result_type=None, location_type=None, - language=None): + language=None, enable_address_descriptor=False): """ Reverse geocoding is the process of converting geographic coordinates into a human-readable address. @@ -87,7 +96,10 @@ def reverse_geocode(client, latlng, result_type=None, location_type=None, :param language: The language in which to return results. :type language: string - :rtype: list of reverse geocoding results. + :rtype: result dict with the following keys: + status: status code + results: list of reverse geocoding results + address_descriptor: address descriptor for the target """ # Check if latlng param is a place_id string. @@ -106,4 +118,7 @@ def reverse_geocode(client, latlng, result_type=None, location_type=None, if language: params["language"] = language - return client._request("/maps/api/geocode/json", params).get("results", []) + if enable_address_descriptor: + params["enable_address_descriptor"] = "true" + + return client._request("/maps/api/geocode/json", params) diff --git a/googlemaps/maps.py b/googlemaps/maps.py index 763e0126..746223d6 100644 --- a/googlemaps/maps.py +++ b/googlemaps/maps.py @@ -20,16 +20,11 @@ from googlemaps import convert -MAPS_IMAGE_FORMATS = set( - ['png8', 'png', 'png32', 'gif', 'jpg', 'jpg-baseline'] -) +MAPS_IMAGE_FORMATS = {'png8', 'png', 'png32', 'gif', 'jpg', 'jpg-baseline'} -MAPS_MAP_TYPES = set( - ['roadmap', 'satellite', 'terrain', 'hybrid'] -) +MAPS_MAP_TYPES = {'roadmap', 'satellite', 'terrain', 'hybrid'} - -class StaticMapParam(object): +class StaticMapParam: """Base class to handle parameters for Maps Static API.""" def __init__(self): @@ -128,7 +123,7 @@ def __init__(self, points, def static_map(client, size, - center=None, zoom=None, scale=None, + center=None, zoom=None, scale=None, format=None, maptype=None, language=None, region=None, markers=None, path=None, visible=None, style=None): """ @@ -186,7 +181,8 @@ def static_map(client, size, :rtype: iterator containing the raw image data, which typically can be used to save an image file locally. For example: - ``` + .. code-block:: python + f = open(local_filename, 'wb') for chunk in client.static_map(size=(400, 400), center=(52.520103, 13.404871), @@ -194,7 +190,6 @@ def static_map(client, size, if chunk: f.write(chunk) f.close() - ``` """ params = {"size": convert.size(size)} diff --git a/googlemaps/places.py b/googlemaps/places.py index af070c4c..269a17fa 100644 --- a/googlemaps/places.py +++ b/googlemaps/places.py @@ -21,9 +21,7 @@ from googlemaps import convert -PLACES_FIND_FIELDS_BASIC = set( - [ - "business_status", +PLACES_FIND_FIELDS_BASIC = {"business_status", "formatted_address", "geometry", "geometry/location", @@ -42,13 +40,11 @@ "photos", "place_id", "plus_code", - "types", - ] -) + "types",} -PLACES_FIND_FIELDS_CONTACT = set(["opening_hours"]) +PLACES_FIND_FIELDS_CONTACT = {"opening_hours"} -PLACES_FIND_FIELDS_ATMOSPHERE = set(["price_level", "rating", "user_ratings_total"]) +PLACES_FIND_FIELDS_ATMOSPHERE = {"price_level", "rating", "user_ratings_total"} PLACES_FIND_FIELDS = ( PLACES_FIND_FIELDS_BASIC @@ -56,43 +52,64 @@ ^ PLACES_FIND_FIELDS_ATMOSPHERE ) -PLACES_DETAIL_FIELDS_BASIC = set( - [ - "address_component", - "adr_address", - "business_status", - "formatted_address", - "geometry", - "geometry/location", - "geometry/location/lat", - "geometry/location/lng", - "geometry/viewport", - "geometry/viewport/northeast", - "geometry/viewport/northeast/lat", - "geometry/viewport/northeast/lng", - "geometry/viewport/southwest", - "geometry/viewport/southwest/lat", - "geometry/viewport/southwest/lng", - "icon", - "name", - "permanently_closed", - "photo", - "place_id", - "plus_code", - "type", - "url", - "utc_offset", - "vicinity", - ] -) - -PLACES_DETAIL_FIELDS_CONTACT = set( - ["formatted_phone_number", "international_phone_number", "opening_hours", "website"] -) - -PLACES_DETAIL_FIELDS_ATMOSPHERE = set( - ["price_level", "rating", "review", "user_ratings_total"] -) +PLACES_DETAIL_FIELDS_BASIC = { + "address_component", + "adr_address", + "business_status", + "formatted_address", + "geometry", + "geometry/location", + "geometry/location/lat", + "geometry/location/lng", + "geometry/viewport", + "geometry/viewport/northeast", + "geometry/viewport/northeast/lat", + "geometry/viewport/northeast/lng", + "geometry/viewport/southwest", + "geometry/viewport/southwest/lat", + "geometry/viewport/southwest/lng", + "icon", + "name", + "permanently_closed", + "photo", + "place_id", + "plus_code", + "type", + "url", + "utc_offset", + "vicinity", + "wheelchair_accessible_entrance" +} + +PLACES_DETAIL_FIELDS_CONTACT = { + "formatted_phone_number", + "international_phone_number", + "opening_hours", + "current_opening_hours", + "secondary_opening_hours", + "website", +} + +PLACES_DETAIL_FIELDS_ATMOSPHERE = { + "curbside_pickup", + "delivery", + "dine_in", + "editorial_summary", + "price_level", + "rating", + "reservable", + "review", # prefer "reviews" to match API documentation + "reviews", + "serves_beer", + "serves_breakfast", + "serves_brunch", + "serves_dinner", + "serves_lunch", + "serves_vegetarian_food", + "serves_wine", + "takeout", + "user_ratings_total" +} PLACES_DETAIL_FIELDS = ( PLACES_DETAIL_FIELDS_BASIC @@ -100,7 +117,7 @@ ^ PLACES_DETAIL_FIELDS_ATMOSPHERE ) -DEPRECATED_FIELDS = {"permanently_closed"} +DEPRECATED_FIELDS = {"permanently_closed", "review"} DEPRECATED_FIELDS_MESSAGE = ( "Fields, %s, are deprecated. " "Read more at https://developers.google.com/maps/deprecations." @@ -180,7 +197,7 @@ def find_place( def places( client, - query, + query=None, location=None, radius=None, language=None, @@ -408,7 +425,15 @@ def _places( return client._request(url, params) -def place(client, place_id, session_token=None, fields=None, language=None): +def place( + client, + place_id, + session_token=None, + fields=None, + language=None, + reviews_no_translations=False, + reviews_sort="most_relevant", +): """ Comprehensive details for an individual place. @@ -428,6 +453,13 @@ def place(client, place_id, session_token=None, fields=None, language=None): :param language: The language in which to return results. :type language: string + :param reviews_no_translations: Specify reviews_no_translations=True to disable translation of reviews; reviews_no_translations=False (default) enables translation of reviews. + :type reviews_no_translations: bool + + :param reviews_sort: The sorting method to use when returning reviews. + Can be set to most_relevant (default) or newest. + :type reviews_sort: string + :rtype: result dict with the following keys: result: dict containing place details html_attributions: set of attributions which must be displayed @@ -456,6 +488,10 @@ def place(client, place_id, session_token=None, fields=None, language=None): params["language"] = language if session_token: params["sessiontoken"] = session_token + if reviews_no_translations: + params["reviews_no_translations"] = "true" + if reviews_sort: + params["reviews_sort"] = reviews_sort return client._request("/maps/api/place/details/json", params) @@ -513,6 +549,7 @@ def places_autocomplete( input_text, session_token=None, offset=None, + origin=None, location=None, radius=None, language=None, @@ -537,6 +574,12 @@ def places_autocomplete( service will match on 'Goo'. :type offset: int + :param origin: The origin point from which to calculate straight-line distance + to the destination (returned as distance_meters). + If this value is omitted, straight-line distance will + not be returned. + :type origin: string, dict, list, or tuple + :param location: The latitude/longitude value for which you wish to obtain the closest, human-readable address. :type location: string, dict, list, or tuple @@ -570,6 +613,7 @@ def places_autocomplete( input_text, session_token=session_token, offset=offset, + origin=origin, location=location, radius=radius, language=language, @@ -623,6 +667,7 @@ def _autocomplete( input_text, session_token=None, offset=None, + origin=None, location=None, radius=None, language=None, @@ -641,6 +686,8 @@ def _autocomplete( params["sessiontoken"] = session_token if offset: params["offset"] = offset + if origin: + params["origin"] = convert.latlng(origin) if location: params["location"] = convert.latlng(location) if radius: diff --git a/noxfile.py b/noxfile.py index 06eefe58..e5b92961 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,20 @@ +# Copyright 2022 Google LLC +# +# 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. + import nox -SUPPORTED_PY_VERSIONS = ["3.5", "3.6", "3.7", "3.8"] +SUPPORTED_PY_VERSIONS = ["3.7", "3.8", "3.9", "3.10"] def _install_dev_packages(session): @@ -38,7 +52,7 @@ def cover(session): session.run("coverage", "erase") -@nox.session(python="3.6") +@nox.session(python="3.7") def docs(session): _install_dev_packages(session) _install_doc_dependencies(session) diff --git a/setup.py b/setup.py index b9b3aeea..df9f32f1 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,17 @@ +# Copyright 2022 Google LLC +# +# 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. + from setuptools import setup @@ -12,7 +26,7 @@ setup( name="googlemaps", - version="4.4.2", + version="4.10.0", description="Python client library for Google Maps Platform", long_description=readme + changelog, long_description_content_type="text/markdown", diff --git a/tests/test_addressvalidation.py b/tests/test_addressvalidation.py new file mode 100644 index 00000000..d1f2f589 --- /dev/null +++ b/tests/test_addressvalidation.py @@ -0,0 +1,48 @@ +# 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 addressvalidation module.""" + +import responses + +import googlemaps +from . import TestCase + + +class AddressValidationTest(TestCase): + def setUp(self): + self.key = "AIzaSyD_sJl0qMA65CYHMBokVfMNA7AKyt5ERYs" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_simple_addressvalidation(self): + responses.add( + responses.POST, + "https://addressvalidation.googleapis.com/v1:validateAddress", + body='{"address": {"regionCode": "US","locality": "Mountain View","addressLines": "1600 Amphitheatre Pkwy"},"enableUspsCass":true}', + status=200, + content_type="application/json", + ) + + results = self.client.addressvalidation('1600 Amphitheatre Pk', regionCode='US', locality='Mountain View', enableUspsCass=True) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://addressvalidation.googleapis.com/v1:validateAddress?" "key=%s" % self.key, + responses.calls[0].request.url, + ) \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index a18221fa..4f01e397 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -280,10 +280,6 @@ def __call__(self, req): self.assertEqual(2, len(responses.calls)) - def test_channel_without_client_id(self): - with self.assertRaises(ValueError): - client = googlemaps.Client(key="AIzaasdf", channel="mychannel") - def test_invalid_channel(self): # Cf. limitations here: # https://developers.google.com/maps/premium/reports diff --git a/tests/test_distance_matrix.py b/tests/test_distance_matrix.py index a906f8ce..6946782e 100644 --- a/tests/test_distance_matrix.py +++ b/tests/test_distance_matrix.py @@ -84,7 +84,10 @@ def test_mixed_params(self): content_type="application/json", ) - origins = ["Bobcaygeon ON", [41.43206, -81.38992]] + origins = [ + "Bobcaygeon ON", [41.43206, -81.38992], + "place_id:ChIJ7cv00DwsDogRAMDACa2m4K8" + ] destinations = [ (43.012486, -83.6964149), {"lat": 42.8863855, "lng": -78.8781627}, @@ -95,7 +98,8 @@ def test_mixed_params(self): self.assertEqual(1, len(responses.calls)) self.assertURLEqual( "https://maps.googleapis.com/maps/api/distancematrix/json?" - "key=%s&origins=Bobcaygeon+ON%%7C41.43206%%2C-81.38992&" + "key=%s&origins=Bobcaygeon+ON%%7C41.43206%%2C-81.38992%%7C" + "place_id%%3AChIJ7cv00DwsDogRAMDACa2m4K8&" "destinations=43.012486%%2C-83.6964149%%7C42.8863855%%2C" "-78.8781627" % self.key, responses.calls[0].request.url, @@ -181,3 +185,34 @@ def test_lang_param(self): "destinations=San+Francisco%%7CVictoria+BC" % self.key, responses.calls[0].request.url, ) + @responses.activate + def test_place_id_param(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/distancematrix/json", + body='{"status":"OK","rows":[]}', + status=200, + content_type="application/json", + ) + + origins = [ + 'place_id:ChIJ7cv00DwsDogRAMDACa2m4K8', + 'place_id:ChIJzxcfI6qAa4cR1jaKJ_j0jhE', + ] + destinations = [ + 'place_id:ChIJPZDrEzLsZIgRoNrpodC5P30', + 'place_id:ChIJjQmTaV0E9YgRC2MLmS_e_mY', + ] + + matrix = self.client.distance_matrix(origins, destinations) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/distancematrix/json?" + "key=%s&" + "origins=place_id%%3AChIJ7cv00DwsDogRAMDACa2m4K8%%7C" + "place_id%%3AChIJzxcfI6qAa4cR1jaKJ_j0jhE&" + "destinations=place_id%%3AChIJPZDrEzLsZIgRoNrpodC5P30%%7C" + "place_id%%3AChIJjQmTaV0E9YgRC2MLmS_e_mY" % self.key, + responses.calls[0].request.url, + ) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py index dfd9376d..8734c8b8 100644 --- a/tests/test_geocoding.py +++ b/tests/test_geocoding.py @@ -41,7 +41,7 @@ def test_simple_geocode(self): content_type="application/json", ) - results = self.client.geocode("Sydney") + results = self.client.geocode("Sydney").get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -60,7 +60,7 @@ def test_reverse_geocode(self): content_type="application/json", ) - results = self.client.reverse_geocode((-33.8674869, 151.2069902)) + results = self.client.reverse_geocode((-33.8674869, 151.2069902)).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -79,7 +79,7 @@ def test_geocoding_the_googleplex(self): content_type="application/json", ) - results = self.client.geocode("1600 Amphitheatre Parkway, " "Mountain View, CA") + results = self.client.geocode("1600 Amphitheatre Parkway, " "Mountain View, CA").get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -105,7 +105,7 @@ def test_geocode_with_bounds(self): "southwest": (34.172684, -118.604794), "northeast": (34.236144, -118.500938), }, - ) + ).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -125,7 +125,7 @@ def test_geocode_with_region_biasing(self): content_type="application/json", ) - results = self.client.geocode("Toledo", region="es") + results = self.client.geocode("Toledo", region="es").get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -144,7 +144,7 @@ def test_geocode_with_component_filter(self): content_type="application/json", ) - results = self.client.geocode("santa cruz", components={"country": "ES"}) + results = self.client.geocode("santa cruz", components={"country": "ES"}).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -165,7 +165,7 @@ def test_geocode_with_multiple_component_filters(self): results = self.client.geocode( "Torun", components={"administrative_area": "TX", "country": "US"} - ) + ).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -191,7 +191,7 @@ def test_geocode_with_just_components(self): "administrative_area": "Helsinki", "country": "Finland", } - ) + ).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -201,6 +201,25 @@ def test_geocode_with_just_components(self): responses.calls[0].request.url, ) + @responses.activate + def test_geocode_place_id(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + results = self.client.geocode(place_id="ChIJeRpOeF67j4AR9ydy_PIzPuM").get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&place_id=ChIJeRpOeF67j4AR9ydy_PIzPuM" % self.key, + responses.calls[0].request.url, + ) + @responses.activate def test_simple_reverse_geocode(self): responses.add( @@ -211,7 +230,7 @@ def test_simple_reverse_geocode(self): content_type="application/json", ) - results = self.client.reverse_geocode((40.714224, -73.961452)) + results = self.client.reverse_geocode((40.714224, -73.961452)).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -234,7 +253,7 @@ def test_reverse_geocode_restricted_by_type(self): (40.714224, -73.961452), location_type="ROOFTOP", result_type="street_address", - ) + ).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -258,7 +277,7 @@ def test_reverse_geocode_multiple_location_types(self): (40.714224, -73.961452), location_type=["ROOFTOP", "RANGE_INTERPOLATED"], result_type="street_address", - ) + ).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -282,7 +301,7 @@ def test_reverse_geocode_multiple_result_types(self): (40.714224, -73.961452), location_type="ROOFTOP", result_type=["street_address", "route"], - ) + ).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -292,6 +311,28 @@ def test_reverse_geocode_multiple_result_types(self): responses.calls[0].request.url, ) + @responses.activate + def test_reverse_geocode_with_address_descriptors(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[], "address_descriptor":{ "landmarks": [ { "placeId": "id" } ] } }', + status=200, + content_type="application/json", + ) + + response = self.client.reverse_geocode((-33.8674869, 151.2069902), enable_address_descriptor=True) + + address_descriptor = response.get("address_descriptor", []) + + self.assertEqual(1, len(address_descriptor["landmarks"])) + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "latlng=-33.8674869,151.2069902&enable_address_descriptor=true&key=%s" % self.key, + responses.calls[0].request.url, + ) + @responses.activate def test_partial_match(self): responses.add( @@ -302,7 +343,7 @@ def test_partial_match(self): content_type="application/json", ) - results = self.client.geocode("Pirrama Pyrmont") + results = self.client.geocode("Pirrama Pyrmont").get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -321,7 +362,7 @@ def test_utf_results(self): content_type="application/json", ) - results = self.client.geocode(components={"postal_code": "96766"}) + results = self.client.geocode(components={"postal_code": "96766"}).get("results", []) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( @@ -340,7 +381,7 @@ def test_utf8_request(self): content_type="application/json", ) - self.client.geocode(self.u("\\u4e2d\\u56fd")) # China + self.client.geocode(self.u("\\u4e2d\\u56fd")).get("results", []) # China self.assertURLEqual( "https://maps.googleapis.com/maps/api/geocode/json?" "key=%s&address=%s" % (self.key, "%E4%B8%AD%E5%9B%BD"), diff --git a/tests/test_places.py b/tests/test_places.py index 5316fe8e..32f03d16 100644 --- a/tests/test_places.py +++ b/tests/test_places.py @@ -163,14 +163,18 @@ def test_place_detail(self): self.client.place( "ChIJN1t_tDeuEmsRUsoyG83frY4", - fields=["business_status", "geometry/location", "place_id"], + fields=["business_status", "geometry/location", + "place_id", "reviews"], language=self.language, + reviews_no_translations=True, + reviews_sort="newest", ) self.assertEqual(1, len(responses.calls)) self.assertURLEqual( "%s?language=en-AU&placeid=ChIJN1t_tDeuEmsRUsoyG83frY4" - "&key=%s&fields=business_status,geometry/location,place_id" + "&reviews_no_translations=true&reviews_sort=newest" + "&key=%s&fields=business_status,geometry/location,place_id,reviews" % (url, self.key), responses.calls[0].request.url, ) @@ -212,6 +216,7 @@ def test_autocomplete(self): "Google", session_token=session_token, offset=3, + origin=self.location, location=self.location, radius=self.radius, language=self.language, @@ -223,6 +228,7 @@ def test_autocomplete(self): self.assertEqual(1, len(responses.calls)) self.assertURLEqual( "%s?components=country%%3Aau&input=Google&language=en-AU&" + "origin=-33.86746%%2C151.20709&" "location=-33.86746%%2C151.20709&offset=3&radius=100&" "strictbounds=true&types=geocode&key=%s&sessiontoken=%s" % (url, self.key, session_token), diff --git a/tests/test_timezone.py b/tests/test_timezone.py index 9d2edc1c..a1d7394e 100644 --- a/tests/test_timezone.py +++ b/tests/test_timezone.py @@ -53,7 +53,7 @@ def test_los_angeles(self): responses.calls[0].request.url, ) - class MockDatetime(object): + class MockDatetime: def now(self): return datetime.datetime.fromtimestamp(1608) diff --git a/text.py b/text.py new file mode 100644 index 00000000..13734488 --- /dev/null +++ b/text.py @@ -0,0 +1,19 @@ +import math + +queries_quota : int +queries_per_second = 60 # None or 60 +queries_per_minute = None # None or 6000 + +try: + if (type(queries_per_second) == int and type(queries_per_minute) == int ): + queries_quota = math.floor(min(queries_per_second, queries_per_minute/60)) + elif (queries_per_second): + queries_quota = math.floor(queries_per_second) + elif (queries_per_minute): + queries_quota = math.floor(queries_per_minute/60) + else: + print("MISSING VALID NUMBER for queries_per_second or queries_per_minute") + print(queries_quota) + +except NameError: + print("MISSING VALUE for queries_per_second or queries_per_minute") \ No newline at end of file