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/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..4782add5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug, triage me' +assignees: '' + +--- + +Thanks for stopping by to let us know something could be better! + +--- +**PLEASE READ** + +If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. + +Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). + +If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). + +Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. + +--- + +Please be sure to include as much information as possible: + +#### Environment details + +1. Specify the API at the beginning of the title (for example, "Places: ...") +2. OS type and version +3. Library version and other environment information + +#### Steps to reproduce + + 1. ? + +#### Code example + +```python +# example +``` + +#### Stack trace +``` +# example +``` + +Following these steps will guarantee the quickest resolution possible. + +Thanks! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..39c3c5ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,35 @@ +--- +name: Feature request +about: Suggest an idea for this library +title: '' +labels: 'type: feature request, triage me' +assignees: '' + +--- + +Thanks for stopping by to let us know something could be better! + +--- +**PLEASE READ** + +If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. + +Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). + +If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). + +Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. + +--- + + **Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + + **Describe the solution you'd like** +A clear and concise description of what you want to happen. + + **Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + + **Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 00000000..495a5c99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,21 @@ +--- +name: Support request +about: If you have a support contract with Google, please create an issue in the Google + Cloud Support console. +title: '' +labels: 'triage me, type: question' +assignees: '' + +--- + +**PLEASE READ** + +If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. + +Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). + +If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). + +Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. + +--- diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..4d7d59e9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,12 @@ +--- +name: Pull request +about: Create a pull request +label: 'triage me' +--- +Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass +- [ ] Code coverage does not decrease (if any source code was changed) +- [ ] Appropriate docs were updated (if necessary) + +Fixes # 🦕 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/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..009707d4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +Thank you for opening a Pull Request! + +--- + +Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass +- [ ] Code coverage does not decrease (if any source code was changed) +- [ ] Appropriate docs were updated (if necessary) + +Fixes # 🦕 diff --git a/.github/scripts/distribution.sh b/.github/scripts/distribution.sh new file mode 100755 index 00000000..e779cd1d --- /dev/null +++ b/.github/scripts/distribution.sh @@ -0,0 +1,20 @@ +#!/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 + +python setup.py sdist +pip install $(find dist -name googlemaps-*.tar.gz) diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh new file mode 100755 index 00000000..39e7f9f0 --- /dev/null +++ b/.github/scripts/install.sh @@ -0,0 +1,28 @@ +#!/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 + +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 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 new file mode 100644 index 00000000..1d39e65d --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,74 @@ +# 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 +daysUntilStale: 120 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 180 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pinned + - "type: bug" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: "stale" + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. Please comment here if it is still valid so that we can + reprioritize. Thank you! + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +closeComment: > + Closing this. Please reopen if you believe it should be addressed. Thank you for your contribution. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 10 + +# Limit to only `issues` or `pulls` +only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed 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/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..a0a28dce --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +# 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. + +name: Release +on: + push: + branches: [ master ] +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@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@v3 + with: + semantic_version: 19 + extra_plugins: | + "@semantic-release/commit-analyzer" + "@semantic-release/release-notes-generator" + "@google/semantic-release-replace-plugin" + "@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 new file mode 100644 index 00000000..570a087a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,56 @@ +# 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 runs tests on every new pull request +name: Run tests + +on: + repository_dispatch: + types: [test] + pull_request: + branches: ['*'] + push: + branches: ['*'] + +jobs: + matrix: + name: "Run tests on Python ${{ matrix.python-version }}" + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Python + uses: "actions/setup-python@v3" + with: + python-version: "${{ matrix.python-version }}" + + - name: Install dependencies + run: ./.github/scripts/install.sh + + - name: Run tests + 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 78c58e12..93e7a2fc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,9 +28,10 @@ dist/ # python testing things etc .coverage -.tox +.nox env -nosetests.xml googlemaps.egg-info *.egg - +.vscode/ +.idea/ +index.py diff --git a/.releaserc b/.releaserc new file mode 100644 index 00000000..5f53e6f5 --- /dev/null +++ b/.releaserc @@ -0,0 +1,23 @@ +branches: + - master +plugins: + - "@semantic-release/commit-analyzer" + - "@semantic-release/release-notes-generator" + - - "@google/semantic-release-replace-plugin" + - replacements: + - files: + - "./googlemaps/__init__.py" + from: "__version__ = \".*\"" + to: "__version__ = \"${nextRelease.version}\"" + - files: + - "./setup.py" + 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" + - "./setup.py" + - "@semantic-release/github" +options: + debug: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ea1fa63d..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: python - -matrix: - include: - - { python: '2.7', env: TOXENV=py27 } - - { python: '3.4', env: TOXENV=py34 } - - { python: '3.5', env: TOXENV=py35 } - - { python: '3.6', env: TOXENV=py36 } - - { python: '3.6', env: TOXENV=docs } - -install: - - pip install requests - - pip install tox - -script: - - tox diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..35deb20a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog +All notable changes to this project will be documented in this file. + +## [v4.2.0] +### Added +- Add support for Maps Static API (#344) + +## [v4.1.0] +### Added +- Adding support for passing in `experience_id` to Client class (#338) + +## [v4.0.0] +### Changed +- Python 2 is no longer supported +- Removed place fields: `alt_id`, `id`, `reference`, and `scope`. Read more about this at https://developers.google.com/maps/deprecations. + +## [v3.1.4] +### Changed +- `APIError.__str__` should always return a str (#328) + +## [v3.1.3] +### Changed +- deprecation warning for place fields: `alt_id`, `id`, `reference`, and `scope`. Read more about this at https://developers.google.com/maps/deprecations. + +## [v3.1.2] +### Added +- Tests for distribution tar as part of CI +- Support for subfields such as `geometry/location` and `geometry/viewport` in Places. + +## [v3.1.1] +### Changed +- Added changelog to manifest + +## [v3.1.0] +### Changed +- Switched build system to use [nox](https://nox.thea.codes/en/stable/), pytest, and codecov. Added Python 3.7 to test framework. +- Set precision of truncated latitude and longitude floats [to 8 decimals](https://github.com/googlemaps/google-maps-services-python/pull/301) instead of 6. +- Minimum version of requests increased. +- Session token parameter [added](https://github.com/googlemaps/google-maps-services-python/pull/244) to `place()`. +- Fixed issue where headers in `request_kwargs` were being overridden. +### Added +- Automation for PyPi uploads. +- Long description to package. +- Added tests to manifest and tarball. +### Removed +- Removed places `places_autocomplete_session_token` which can be replaced with `uuid.uuid4().hex`. +- Removed deprecated `places_radar`. + + +**Note:** Start of changelog is 2019-08-27, [v3.0.2]. + +[Unreleased]: https://github.com/googlemaps/google-maps-services-python/compare/4.2.0...HEAD +[v4.2.0]: https://github.com/googlemaps/google-maps-services-python/compare/4.1.0...4.2.0 +[v4.1.0]: https://github.com/googlemaps/google-maps-services-python/compare/4.0.0...4.1.0 +[v4.0.0]: https://github.com/googlemaps/google-maps-services-python/compare/3.1.4...4.0.0 +[v3.1.4]: https://github.com/googlemaps/google-maps-services-python/compare/3.1.3...3.1.4 +[v3.1.3]: https://github.com/googlemaps/google-maps-services-python/compare/3.1.2...3.1.3 +[v3.1.2]: https://github.com/googlemaps/google-maps-services-python/compare/3.1.1...3.1.2 +[v3.1.1]: https://github.com/googlemaps/google-maps-services-python/compare/3.1.0...3.1.1 +[v3.1.0]: https://github.com/googlemaps/google-maps-services-python/compare/3.0.2...3.1.0 +[v3.0.2]: https://github.com/googlemaps/google-maps-services-python/compare/3.0.1...3.0.2 +[v3.0.1]: https://github.com/googlemaps/google-maps-services-python/compare/3.0.0...3.0.1 +[v3.0.0]: https://github.com/googlemaps/google-maps-services-python/compare/2.5.1...3.0.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f8b12cb5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,63 @@ +# Google Open Source Community Guidelines + +At Google, we recognize and celebrate the creativity and collaboration of open +source contributors and the diversity of skills, experiences, cultures, and +opinions they bring to the projects and communities they participate in. + +Every one of Google's open source projects and communities are inclusive +environments, based on treating all individuals respectfully, regardless of +gender identity and expression, sexual orientation, disabilities, +neurodiversity, physical appearance, body size, ethnicity, nationality, race, +age, religion, or similar personal characteristic. + +We value diverse opinions, but we value respectful behavior more. + +Respectful behavior includes: + +* Being considerate, kind, constructive, and helpful. +* Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or + physically threatening behavior, speech, and imagery. +* Not engaging in unwanted physical contact. + +Some Google open source projects [may adopt][] an explicit project code of +conduct, which may have additional detailed expectations for participants. Most +of those projects will use our [modified Contributor Covenant][]. + +[may adopt]: https://opensource.google/docs/releasing/preparing/#conduct +[modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ + +## Resolve peacefully + +We do not believe that all conflict is necessarily bad; healthy debate and +disagreement often yields positive results. However, it is never okay to be +disrespectful. + +If you see someone behaving disrespectfully, you are encouraged to address the +behavior directly with those involved. Many issues can be resolved quickly and +easily, and this gives people more control over the outcome of their dispute. +If you are unable to resolve the matter for any reason, or if the behavior is +threatening or harassing, report it. We are dedicated to providing an +environment where participants feel welcome and safe. + +## Reporting problems + +Some Google open source projects may adopt a project-specific code of conduct. +In those cases, a Google employee will be identified as the Project Steward, +who will receive and handle reports of code of conduct violations. In the event +that a project hasn’t identified a Project Steward, you can report problems by +emailing opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is +taken. The identity of the reporter will be omitted from the details of the +report supplied to the accused. In potentially harmful situations, such as +ongoing harassment or threats to anyone's safety, we may take action without +notice. + +*This document was adapted from the [IndieWeb Code of Conduct][] and can also +be found at .* + +[IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct diff --git a/MANIFEST.in b/MANIFEST.in index 4e833047..b248a6b8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE README.md +include CHANGELOG.md LICENSE README.md global-exclude __pycache__ global-exclude *.py[co] diff --git a/README.md b/README.md index e9a2e9c7..40823790 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,32 @@ 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) +![GitHub contributors](https://img.shields.io/github/contributors/googlemaps/google-maps-services-python) ## Description -Use Python? Want to [geocode][Geocoding API] something? Looking for [directions][Directions API]? -Maybe [matrices of directions][Distance Matrix API]? This library brings the [Google Maps API Web -Services] to your Python application. -![Analytics](https://maps-ga-beacon.appspot.com/UA-12846745-20/google-maps-services-python/readme?pixel) +Use Python? Want to geocode something? Looking for directions? +Maybe matrices of directions? This library brings the Google Maps Platform Web +Services to your Python application. The Python Client for Google Maps Services is a Python Client library for the following Google Maps APIs: - - [Directions API] - - [Distance Matrix API] - - [Elevation API] - - [Geocoding API] - - [Geolocation API] - - [Time Zone API] - - [Roads API] - - [Places API] + - Directions API + - Distance Matrix API + - Elevation API + - Geocoding API + - Geolocation API + - Time Zone API + - 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. @@ -35,42 +41,20 @@ to make backwards-incompatible changes. If we do remove some functionality (typi better functionality exists or if the feature proved infeasible), our intention is to deprecate and give developers a year to update their code. -If you find a bug, or have a feature suggestion, please [log an issue][issues]. If you'd like to -contribute, please read [How to Contribute][contrib]. +If you find a bug, or have a feature suggestion, please log an issue. If you'd like to +contribute, please read contribute. ## Requirements - - Python 2.7 or later. + - Python 3.5 or later. - A Google Maps API key. -### API keys +## API Keys Each Google Maps Web Service request requires an API key or client ID. API keys -are freely available with a Google Account at -https://developers.google.com/console. The type of API key you need is a -**Server key**. - -To get an API key: - - 1. Visit https://developers.google.com/console and log in with - a Google Account. - 1. Select one of your existing projects, or create a new project. - 1. Enable the API(s) you want to use. The Python Client for Google Maps Services - accesses the following APIs: - * Directions API - * Distance Matrix API - * Elevation API - * Geocoding API - * Geolocation API - * Places API - * Roads API - * Time Zone API - 1. Create a new **Server key**. - 1. If you'd like to restrict requests to a specific IP address, do so now. - -For guided help, follow the instructions for the [Directions API][directions-key]. -You only need one API key, but remember to enable all the APIs you need. -For even more information, see the guide to [API keys][apikey]. +are generated in the 'Credentials' page of the 'APIs & Services' tab of [Google Cloud console](https://console.cloud.google.com/apis/credentials). + +For even more information on getting started with Google Maps Platform and generating/restricting an API key, see [Get Started with Google Maps Platform](https://developers.google.com/maps/gmp-get-started) in our docs. **Important:** This key should be kept secret on your server. @@ -80,25 +64,9 @@ For even more information, see the guide to [API keys][apikey]. Note that you will need requests 2.4.0 or higher if you want to specify connect/read timeouts. -## Developer Documentation - -View the [reference documentation](https://googlemaps.github.io/google-maps-services-python/docs/) - -Additional documentation for the included web services is available at -https://developers.google.com/maps/. - - - [Directions API] - - [Distance Matrix API] - - [Elevation API] - - [Geocoding API] - - [Geolocation API] - - [Time Zone API] - - [Roads API] - - [Places API] - ## Usage -This example uses the [Geocoding API] and the [Directions API] with an API key: +This example uses the Geocoding API and the Directions API with an API key: ```python import googlemaps @@ -118,34 +86,19 @@ directions_result = gmaps.directions("Sydney Town Hall", "Parramatta, NSW", mode="transit", departure_time=now) -``` -Below is the same example, using client ID and client secret (digital signature) -for authentication. This code assumes you have previously loaded the `client_id` -and `client_secret` variables with appropriate values. +# Validate an address with address validation +addressvalidation_result = gmaps.addressvalidation(['1600 Amphitheatre Pk'], + regionCode='US', + locality='Mountain View', + enableUspsCass=True) -For a guide on how to generate the `client_secret` (digital signature), see the -documentation for the API you're using. For example, see the guide for the -[Directions API][directions-client-id]. +# 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) -```python -gmaps = googlemaps.Client(client_id=client_id, client_secret=client_secret) - -# Geocoding and address -geocode_result = gmaps.geocode('1600 Amphitheatre Parkway, Mountain View, CA') - -# Look up an address with reverse geocoding -reverse_geocode_result = gmaps.reverse_geocode((40.714224, -73.961452)) - -# Request directions via public transit -now = datetime.now() -directions_result = gmaps.directions("Sydney Town Hall", - "Parramatta, NSW", - mode="transit", - departure_time=now) ``` -For more usage examples, check out [the tests](googlemaps/test/). +For more usage examples, check out [the tests](https://github.com/googlemaps/google-maps-services-python/tree/master/tests). ## Features @@ -154,46 +107,44 @@ For more usage examples, check out [the tests](googlemaps/test/). Automatically retry when intermittent failures occur. That is, when any of the retriable 5xx errors are returned from the API. -### Client IDs - -Google Maps APIs Premium Plan customers can use their [client ID and secret][clientid] to authenticate, -instead of an API key. ## Building the Project - # Installing tox - $ pip install tox + # Installing nox + $ pip install nox # Running tests - $ tox + $ nox # Generating documentation - $ tox -e docs - - # Uploading a new release - $ easy_install wheel twine - $ python setup.py sdist bdist_wheel - $ twine upload dist/* + $ nox -e docs # Copy docs to gh-pages - $ tox -e docs && mv docs/_build/html generated_docs && git clean -Xdi && git checkout gh-pages - - -[apikey]: https://developers.google.com/maps/faq#keysystem -[clientid]: https://developers.google.com/maps/documentation/business/webservices/auth - -[Google Maps API Web Services]: https://developers.google.com/maps/apis-by-platform#web_service_apis -[Directions API]: https://developers.google.com/maps/documentation/directions/ -[directions-key]: https://developers.google.com/maps/documentation/directions/get-api-key#key -[directions-client-id]: https://developers.google.com/maps/documentation/directions/get-api-key#client-id -[Distance Matrix API]: https://developers.google.com/maps/documentation/distancematrix/ -[Elevation API]: https://developers.google.com/maps/documentation/elevation/ -[Geocoding API]: https://developers.google.com/maps/documentation/geocoding/ -[Geolocation API]: https://developers.google.com/maps/documentation/geolocation/ -[Time Zone API]: https://developers.google.com/maps/documentation/timezone/ -[Roads API]: https://developers.google.com/maps/documentation/roads/ -[Places API]: https://developers.google.com/places/ - -[issues]: https://github.com/googlemaps/google-maps-services-python/issues -[contrib]: https://github.com/googlemaps/google-maps-services-python/blob/master/CONTRIB.md + $ nox -e docs && mv docs/_build/html generated_docs && git clean -Xdi && git checkout gh-pages + +## Documentation & resources + +[Documentation for the `google-maps-services-python` library](https://googlemaps.github.io/google-maps-services-python/docs/index.html) + +### Getting started +- [Get Started with Google Maps Platform](https://developers.google.com/maps/gmp-get-started) +- [Generating/restricting an API key](https://developers.google.com/maps/gmp-get-started#api-key) +- [Authenticating with a client ID](https://developers.google.com/maps/documentation/directions/get-api-key#client-id) + +### API docs +- [Google Maps Platform web services](https://developers.google.com/maps/apis-by-platform#web_service_apis) +- [Directions API](https://developers.google.com/maps/documentation/directions/) +- [Distance Matrix API](https://developers.google.com/maps/documentation/distancematrix/) +- [Elevation API](https://developers.google.com/maps/documentation/elevation/) +- [Geocoding API](https://developers.google.com/maps/documentation/geocoding/) +- [Geolocation API](https://developers.google.com/maps/documentation/geolocation/) +- [Time Zone API](https://developers.google.com/maps/documentation/timezone/) +- [Roads API](https://developers.google.com/maps/documentation/roads/) +- [Places API](https://developers.google.com/places/) +- [Maps Static API](https://developers.google.com/maps/documentation/maps-static/) + +### Support +- [Report an issue](https://github.com/googlemaps/google-maps-services-python/issues) +- [Contribute](https://github.com/googlemaps/google-maps-services-python/blob/master/CONTRIB.md) +- [StackOverflow](http://stackoverflow.com/questions/tagged/google-maps) 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 63f904c8..61ec45d0 100644 --- a/googlemaps/__init__.py +++ b/googlemaps/__init__.py @@ -15,10 +15,10 @@ # the License. # -__version__ = "3.0.2" +__version__ = "4.10.0" from googlemaps.client import Client -import googlemaps.exceptions +from googlemaps import exceptions -# Allow sphinx to pick up these symbols for the documentation. -__all__ = ["Client"] + +__all__ = ["Client", "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 6c695f31..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,30 +42,36 @@ 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, - retry_over_query_limit=True): + 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 - "client_secret" are set. + "client_secret" are set. Most users should use an API key. :type key: string :param client_id: (for Maps API for Work customers) Your client ID. + Most users should use an API key instead. :type client_id: string :param client_secret: (for Maps API for Work customers) Your client - secret (base64 encoded). + secret (base64 encoded). Most users should use an API key instead. :type client_secret: string :param channel: (for Maps API for Work customers) When set, a channel @@ -89,16 +98,25 @@ 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. :type retry_over_query_limit: bool + :param experience_id: The value for the HTTP header field name + 'X-Goog-Maps-Experience-ID'. + :type experience_id: str + :raises ValueError: when either credentials are missing, incomplete or invalid. :raises NotImplementedError: if connect_timeout and read_timeout are @@ -110,6 +128,13 @@ def __init__(self, key=None, client_id=None, client_secret=None, 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. + :type base_url: string + """ if not key and not (client_secret and client_id): raise ValueError("Must provide API key or enterprise credentials " @@ -119,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): @@ -150,19 +173,71 @@ def __init__(self, key=None, client_id=None, client_secret=None, self.retry_timeout = timedelta(seconds=retry_timeout) self.requests_kwargs = requests_kwargs or {} headers = self.requests_kwargs.pop('headers', {}) - headers.update({"User-Agent": _USER_AGENT}) + headers.update({"User-Agent": _USER_AGENT}) self.requests_kwargs.update({ "headers": headers, "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 + + def set_experience_id(self, *experience_id_args): + """Sets the value for the HTTP header field name + 'X-Goog-Maps-Experience-ID' to be used on subsequent API calls. + + :param experience_id_args: the experience ID + :type experience_id_args: string varargs + """ + if len(experience_id_args) == 0 or experience_id_args[0] is None: + self.clear_experience_id() + return + + headers = self.requests_kwargs.pop("headers", {}) + headers[_X_GOOG_MAPS_EXPERIENCE_ID] = ",".join(experience_id_args) + self.requests_kwargs["headers"] = headers + + def get_experience_id(self): + """Gets the experience ID for the HTTP header field name + 'X-Goog-Maps-Experience-ID' + + :return: The experience ID if set + :rtype: str + """ + headers = self.requests_kwargs.get("headers", {}) + return headers.get(_X_GOOG_MAPS_EXPERIENCE_ID, None) + + def clear_experience_id(self): + """Clears the experience ID for the HTTP header field name + 'X-Goog-Maps-Experience-ID' if set. + """ + headers = self.requests_kwargs.get("headers") + if headers is None: + return + headers.pop(_X_GOOG_MAPS_EXPERIENCE_ID, {}) + self.requests_kwargs["headers"] = headers def _request(self, url, params, first_request_time=None, retry_counter=0, - base_url=_DEFAULT_BASE_URL, accepts_clientid=True, + base_url=None, accepts_clientid=True, extract_body=None, requests_kwargs=None, post_json=None): """Performs HTTP GET/POST with credentials, returning the body as JSON. @@ -204,6 +279,9 @@ def _request(self, url, params, first_request_time=None, retry_counter=0, exceute a request. """ + if base_url is None: + base_url = self.base_url + if not first_request_time: first_request_time = datetime.now() @@ -250,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) @@ -344,12 +422,12 @@ def _generate_auth_url(self, path, params, accepts_clientid): from googlemaps.places import find_place from googlemaps.places import places from googlemaps.places import places_nearby -from googlemaps.places import places_radar from googlemaps.places import place from googlemaps.places import places_photo 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): """ @@ -388,11 +466,12 @@ def wrapper(*args, **kwargs): Client.find_place = make_api_method(find_place) Client.places = make_api_method(places) Client.places_nearby = make_api_method(places_nearby) -Client.places_radar = make_api_method(places_radar) Client.place = make_api_method(place) Client.places_photo = make_api_method(places_photo) 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): @@ -423,11 +502,17 @@ def urlencode_params(params): """ # urlencode does not handle unicode strings in Python 2. # Firstly, normalize the values so they get encoded correctly. - params = [(key, normalize_for_urlencode(val)) for key, val in params] + extended = [] + for key, val in params: + if isinstance(val, (list, tuple)): + for v in val: + extended.append((key, normalize_for_urlencode(v))) + else: + extended.append((key, normalize_for_urlencode(val))) # Secondly, unquote unreserved chars which are incorrectly quoted # by urllib.urlencode, causing invalid auth signatures. See GH #72 # for more info. - return requests.utils.unquote_unreserved(urlencode(params)) + return requests.utils.unquote_unreserved(urlencode(extended)) try: @@ -449,4 +534,7 @@ def normalize_for_urlencode(value): def normalize_for_urlencode(value): """(Python 3) No-op.""" # urlencode in Python 3 handles all the types we are passing it. - return value + if isinstance(value, str): + return value + + return normalize_for_urlencode(str(value)) diff --git a/googlemaps/convert.py b/googlemaps/convert.py index a20e3a32..2b3d056e 100644 --- a/googlemaps/convert.py +++ b/googlemaps/convert.py @@ -28,15 +28,14 @@ # '-33.8674869,151.2069902' """ -import time as _time - def format_float(arg): """Formats a float value to be as short as possible. - Trims extraneous trailing zeros and period to give API - args the best possible chance of fitting within 2000 char - URL length restrictions. + Truncates float to 8 decimal places and trims extraneous + trailing zeros and period to give API args the best + possible chance of fitting within 2000 char URL length + restrictions. For example: @@ -45,13 +44,15 @@ def format_float(arg): format_float(40.1) -> "40.1" format_float(40.001) -> "40.001" format_float(40.0010) -> "40.001" + format_float(40.000000001) -> "40" + format_float(40.000000009) -> "40.00000001" :param arg: The lat or lng float. :type arg: float :rtype: string """ - return ("%f" % float(arg)).rstrip("0").rstrip(".") + return ("%.8f" % float(arg)).rstrip("0").rstrip(".") def latlng(arg): @@ -159,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): @@ -184,8 +183,8 @@ def time(arg): :type arg: datetime.datetime or int """ # handle datetime instances. - if _has_method(arg, "timetuple"): - arg = _time.mktime(arg.timetuple()) + if _has_method(arg, "timestamp"): + arg = arg.timestamp() if isinstance(arg, float): arg = int(arg) @@ -277,6 +276,17 @@ def bounds(arg): "but got %s" % type(arg).__name__) +def size(arg): + if isinstance(arg, int): + return "%sx%s" % (arg, arg) + elif _is_list(arg): + return "%sx%s" % (arg[0], arg[1]) + + raise TypeError( + "Expected a size int or list, " + "but got %s" % type(arg).__name__) + + def decode_polyline(polyline): """Decodes a Polyline string into a list of lat/lng dicts. diff --git a/googlemaps/directions.py b/googlemaps/directions.py index f1713dfb..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 @@ -41,7 +41,9 @@ def directions(client, origin, destination, :type mode: string :param waypoints: Specifies an array of waypoints. Waypoints alter a - route by routing it through the specified location(s). + route by routing it through the specified location(s). To influence + route without adding stop prefix the waypoint with `via`, similar to + `waypoints = ["via:San Francisco", "via:Mountain View"]`. :type waypoints: a single location, or a list of locations, where a location is a string, dict, list, or tuple diff --git a/googlemaps/distance_matrix.py b/googlemaps/distance_matrix.py index f6a85e8c..a30cbe09 100755 --- a/googlemaps/distance_matrix.py +++ b/googlemaps/distance_matrix.py @@ -18,7 +18,6 @@ """Performs requests to the Google Maps Distance Matrix API.""" from googlemaps import convert -from googlemaps.convert import as_list def distance_matrix(client, origins, destinations, @@ -27,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/exceptions.py b/googlemaps/exceptions.py index 679b26c3..0a0f116a 100644 --- a/googlemaps/exceptions.py +++ b/googlemaps/exceptions.py @@ -27,7 +27,7 @@ def __init__(self, status, message=None): def __str__(self): if self.message is None: - return self.status + return str(self.status) else: return "%s (%s)" % (self.status, self.message) 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 new file mode 100644 index 00000000..746223d6 --- /dev/null +++ b/googlemaps/maps.py @@ -0,0 +1,247 @@ +# +# Copyright 2020 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 Static API.""" + +from googlemaps import convert + + +MAPS_IMAGE_FORMATS = {'png8', 'png', 'png32', 'gif', 'jpg', 'jpg-baseline'} + +MAPS_MAP_TYPES = {'roadmap', 'satellite', 'terrain', 'hybrid'} + +class StaticMapParam: + """Base class to handle parameters for Maps Static API.""" + + def __init__(self): + self.params = [] + + def __str__(self): + """Converts a list of parameters to the format expected by + the Google Maps server. + + :rtype: str + + """ + return convert.join_list('|', self.params) + + +class StaticMapMarker(StaticMapParam): + """Handles marker parameters for Maps Static API.""" + + def __init__(self, locations, + size=None, color=None, label=None): + """ + :param locations: Specifies the locations of the markers on + the map. + :type locations: list + + :param size: Specifies the size of the marker. + :type size: str + + :param color: Specifies a color of the marker. + :type color: str + + :param label: Specifies a single uppercase alphanumeric + character to be displaied on marker. + :type label: str + """ + + super(StaticMapMarker, self).__init__() + + if size: + self.params.append("size:%s" % size) + + if color: + self.params.append("color:%s" % color) + + if label: + if len(label) != 1 or (label.isalpha() and not label.isupper()) or not label.isalnum(): + raise ValueError("Marker label must be alphanumeric and uppercase.") + self.params.append("label:%s" % label) + + self.params.append(convert.location_list(locations)) + + +class StaticMapPath(StaticMapParam): + """Handles path parameters for Maps Static API.""" + + def __init__(self, points, + weight=None, color=None, + fillcolor=None, geodesic=None): + """ + :param points: Specifies the point through which the path + will be built. + :type points: list + + :param weight: Specifies the thickness of the path in pixels. + :type weight: int + + :param color: Specifies a color of the path. + :type color: str + + :param fillcolor: Indicates both that the path marks off a + polygonal area and specifies the fill color to use as an + overlay within that area. + :type fillcolor: str + + :param geodesic: Indicates that the requested path should be + interpreted as a geodesic line that follows the curvature + of the earth. + :type geodesic: bool + """ + + super(StaticMapPath, self).__init__() + + if weight: + self.params.append("weight:%s" % weight) + + if color: + self.params.append("color:%s" % color) + + if fillcolor: + self.params.append("fillcolor:%s" % fillcolor) + + if geodesic: + self.params.append("geodesic:%s" % geodesic) + + self.params.append(convert.location_list(points)) + + +def static_map(client, size, + center=None, zoom=None, scale=None, + format=None, maptype=None, language=None, region=None, + markers=None, path=None, visible=None, style=None): + """ + Downloads a map image from the Maps Static API. + + See https://developers.google.com/maps/documentation/maps-static/intro + for more info, including more detail for each parameter below. + + :param size: Defines the rectangular dimensions of the map image. + :type param: int or list + + :param center: Defines the center of the map, equidistant from all edges + of the map. + :type center: dict or list or string + + :param zoom: Defines the zoom level of the map, which determines the + magnification level of the map. + :type zoom: int + + :param scale: Affects the number of pixels that are returned. + :type scale: int + + :param format: Defines the format of the resulting image. + :type format: string + + :param maptype: defines the type of map to construct. There are several + possible maptype values, including roadmap, satellite, hybrid, + and terrain. + :type maptype: string + + :param language: defines the language to use for display of labels on + map tiles. + :type language: string + + :param region: defines the appropriate borders to display, based on + geo-political sensitivities. + :type region: string + + :param markers: define one or more markers to attach to the image at + specified locations. + :type markers: StaticMapMarker + + :param path: defines a single path of two or more connected points to + overlay on the image at specified locations. + :type path: StaticMapPath + + :param visible: specifies one or more locations that should remain visible + on the map, though no markers or other indicators will be displayed. + :type visible: list of dict + + :param style: defines a custom style to alter the presentation of + a specific feature (roads, parks, and other features) of the map. + :type style: list of dict + + :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), + zoom=15): + if chunk: + f.write(chunk) + f.close() + """ + + params = {"size": convert.size(size)} + + if not markers: + if not (center or zoom is not None): + raise ValueError( + "both center and zoom are required" + "when markers is not specifed" + ) + + if center: + params["center"] = convert.latlng(center) + + if zoom is not None: + params["zoom"] = zoom + + if scale is not None: + params["scale"] = scale + + if format: + if format not in MAPS_IMAGE_FORMATS: + raise ValueError("Invalid image format") + params['format'] = format + + if maptype: + if maptype not in MAPS_MAP_TYPES: + raise ValueError("Invalid maptype") + params["maptype"] = maptype + + if language: + params["language"] = language + + if region: + params["region"] = region + + if markers: + params["markers"] = markers + + if path: + params["path"] = path + + if visible: + params["visible"] = convert.location_list(visible) + + if style: + params["style"] = convert.components(style) + + response = client._request( + "/maps/api/staticmap", + params, + extract_body=lambda response: response, + requests_kwargs={"stream": True}, + ) + return response.iter_content() diff --git a/googlemaps/places.py b/googlemaps/places.py index 7c2277a9..269a17fa 100644 --- a/googlemaps/places.py +++ b/googlemaps/places.py @@ -16,45 +16,117 @@ # """Performs requests to the Google Places API.""" +import warnings -from uuid import uuid4 as places_autocomplete_session_token from googlemaps import convert -PLACES_FIND_FIELDS_BASIC = set([ - "formatted_address", "geometry", "icon", "id", "name", - "permanently_closed", "photos", "place_id", "plus_code", "scope", - "types", -]) - -PLACES_FIND_FIELDS_CONTACT = set(["opening_hours",]) - -PLACES_FIND_FIELDS_ATMOSPHERE = set(["price_level", "rating"]) - -PLACES_FIND_FIELDS = (PLACES_FIND_FIELDS_BASIC ^ - PLACES_FIND_FIELDS_CONTACT ^ - PLACES_FIND_FIELDS_ATMOSPHERE) - -PLACES_DETAIL_FIELDS_BASIC = set([ - "address_component", "adr_address", "alt_id", "formatted_address", - "geometry", "icon", "id", "name", "permanently_closed", "photo", - "place_id", "plus_code", "scope", "type", "url", "utc_offset", "vicinity", -]) - -PLACES_DETAIL_FIELDS_CONTACT = set([ - "formatted_phone_number", "international_phone_number", "opening_hours", +PLACES_FIND_FIELDS_BASIC = {"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", + "photos", + "place_id", + "plus_code", + "types",} + +PLACES_FIND_FIELDS_CONTACT = {"opening_hours"} + +PLACES_FIND_FIELDS_ATMOSPHERE = {"price_level", "rating", "user_ratings_total"} + +PLACES_FIND_FIELDS = ( + PLACES_FIND_FIELDS_BASIC + ^ PLACES_FIND_FIELDS_CONTACT + ^ PLACES_FIND_FIELDS_ATMOSPHERE +) + +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 = set(["price_level", "rating", "review",]) - -PLACES_DETAIL_FIELDS = (PLACES_DETAIL_FIELDS_BASIC ^ - PLACES_DETAIL_FIELDS_CONTACT ^ - PLACES_DETAIL_FIELDS_ATMOSPHERE) - - -def find_place(client, input, input_type, fields=None, location_bias=None, - language=None): +} + +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 + ^ PLACES_DETAIL_FIELDS_CONTACT + ^ PLACES_DETAIL_FIELDS_ATMOSPHERE +) + +DEPRECATED_FIELDS = {"permanently_closed", "review"} +DEPRECATED_FIELDS_MESSAGE = ( + "Fields, %s, are deprecated. " + "Read more at https://developers.google.com/maps/deprecations." +) + + +def find_place( + client, input, input_type, fields=None, location_bias=None, language=None +): """ A Find Place request takes a text input, and returns a place. The text input can be any kind of Places data, for example, @@ -68,10 +140,9 @@ def find_place(client, input, input_type, fields=None, location_bias=None, or 'phonenumber'. :type input_type: string - :param fields: The fields specifying the types of place data to return, - separated by a comma. For full details see: + :param fields: The fields specifying the types of place data to return. For full details see: https://developers.google.com/places/web-service/search#FindPlaceRequests - :type input: list + :type fields: list :param location_bias: Prefer results in a specified area, by specifying either a radius plus lat/lng, or two lat/lng pairs @@ -89,25 +160,34 @@ def find_place(client, input, input_type, fields=None, location_bias=None, params = {"input": input, "inputtype": input_type} if input_type != "textquery" and input_type != "phonenumber": - raise ValueError("Valid values for the `input_type` param for " - "`find_place` are 'textquery' or 'phonenumber', " - "the given value is invalid: '%s'" % input_type) + raise ValueError( + "Valid values for the `input_type` param for " + "`find_place` are 'textquery' or 'phonenumber', " + "the given value is invalid: '%s'" % input_type + ) if fields: + deprecated_fields = set(fields) & DEPRECATED_FIELDS + if deprecated_fields: + warnings.warn( + DEPRECATED_FIELDS_MESSAGE % str(list(deprecated_fields)), + DeprecationWarning, + ) + invalid_fields = set(fields) - PLACES_FIND_FIELDS if invalid_fields: - raise ValueError("Valid values for the `fields` param for " - "`find_place` are '%s', these given field(s) " - "are invalid: '%s'" % ( - "', '".join(PLACES_FIND_FIELDS), - "', '".join(invalid_fields))) + raise ValueError( + "Valid values for the `fields` param for " + "`find_place` are '%s', these given field(s) " + "are invalid: '%s'" + % ("', '".join(PLACES_FIND_FIELDS), "', '".join(invalid_fields)) + ) params["fields"] = convert.join_list(",", fields) if location_bias: valid = ["ipbias", "point", "circle", "rectangle"] if location_bias.split(":")[0] not in valid: - raise ValueError("location_bias should be prefixed with one of: %s" - % valid) + raise ValueError("location_bias should be prefixed with one of: %s" % valid) params["locationbias"] = location_bias if language: params["language"] = language @@ -115,9 +195,19 @@ def find_place(client, input, input_type, fields=None, location_bias=None, return client._request("/maps/api/place/findplacefromtext/json", params) -def places(client, query, location=None, radius=None, language=None, - min_price=None, max_price=None, open_now=False, type=None, region=None, - page_token=None): +def places( + client, + query=None, + location=None, + radius=None, + language=None, + min_price=None, + max_price=None, + open_now=False, + type=None, + region=None, + page_token=None, +): """ Places search. @@ -166,15 +256,36 @@ def places(client, query, location=None, radius=None, language=None, html_attributions: set of attributions which must be displayed next_page_token: token for retrieving the next page of results """ - return _places(client, "text", query=query, location=location, - radius=radius, language=language, min_price=min_price, - max_price=max_price, open_now=open_now, type=type, region=region, - page_token=page_token) - - -def places_nearby(client, location=None, radius=None, keyword=None, - language=None, min_price=None, max_price=None, name=None, - open_now=False, rank_by=None, type=None, page_token=None): + return _places( + client, + "text", + query=query, + location=location, + radius=radius, + language=language, + min_price=min_price, + max_price=max_price, + open_now=open_now, + type=type, + region=region, + page_token=page_token, + ) + + +def places_nearby( + client, + location=None, + radius=None, + keyword=None, + language=None, + min_price=None, + max_price=None, + name=None, + open_now=False, + rank_by=None, + type=None, + page_token=None, +): """ Performs nearby search for places. @@ -237,79 +348,51 @@ def places_nearby(client, location=None, radius=None, keyword=None, raise ValueError("either a location or page_token arg is required") if rank_by == "distance": if not (keyword or name or type): - raise ValueError("either a keyword, name, or type arg is required " - "when rank_by is set to distance") + raise ValueError( + "either a keyword, name, or type arg is required " + "when rank_by is set to distance" + ) elif radius is not None: - raise ValueError("radius cannot be specified when rank_by is set to " - "distance") - - return _places(client, "nearby", location=location, radius=radius, - keyword=keyword, language=language, min_price=min_price, - max_price=max_price, name=name, open_now=open_now, - rank_by=rank_by, type=type, page_token=page_token) - - -def places_radar(client, location, radius, keyword=None, min_price=None, - max_price=None, name=None, open_now=False, type=None): - """ - Performs radar search for places. - - :param location: The latitude/longitude value for which you wish to obtain the - closest, human-readable address. - :type location: string, dict, list, or tuple - - :param radius: Distance in meters within which to bias results. - :type radius: int - - :param keyword: A term to be matched against all content that Google has - indexed for this place. - :type keyword: string - - :param min_price: Restricts results to only those places with no less than - this price level. Valid values are in the range from 0 - (most affordable) to 4 (most expensive). - :type min_price: int - - :param max_price: Restricts results to only those places with no greater - than this price level. Valid values are in the range - from 0 (most affordable) to 4 (most expensive). - :type max_price: int - - :param name: One or more terms to be matched against the names of places. - :type name: string or list of strings - - :param open_now: Return only those places that are open for business at - the time the query is sent. - :type open_now: bool - - :param type: Restricts the results to places matching the specified type. - The full list of supported types is available here: - https://developers.google.com/places/supported_types - :type type: string - - :rtype: result dict with the following keys: - status: status code - results: list of places - html_attributions: set of attributions which must be displayed - - """ - if not (keyword or name or type): - raise ValueError("either a keyword, name, or type arg is required") - - from warnings import warn - warn("places_radar is deprecated, see http://goo.gl/BGiumE", - DeprecationWarning) - - return _places(client, "radar", location=location, radius=radius, - keyword=keyword, min_price=min_price, max_price=max_price, - name=name, open_now=open_now, type=type) - - -def _places(client, url_part, query=None, location=None, radius=None, - keyword=None, language=None, min_price=0, max_price=4, name=None, - open_now=False, rank_by=None, type=None, region=None, page_token=None): + raise ValueError( + "radius cannot be specified when rank_by is set to " "distance" + ) + + return _places( + client, + "nearby", + location=location, + radius=radius, + keyword=keyword, + language=language, + min_price=min_price, + max_price=max_price, + name=name, + open_now=open_now, + rank_by=rank_by, + type=type, + page_token=page_token, + ) + + +def _places( + client, + url_part, + query=None, + location=None, + radius=None, + keyword=None, + language=None, + min_price=0, + max_price=4, + name=None, + open_now=False, + rank_by=None, + type=None, + region=None, + page_token=None, +): """ - Internal handler for ``places``, ``places_nearby``, and ``places_radar``. + Internal handler for ``places`` and ``places_nearby``. See each method's docs for arg details. """ @@ -342,7 +425,15 @@ def _places(client, url_part, query=None, location=None, radius=None, 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. @@ -362,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 @@ -369,19 +467,31 @@ def place(client, place_id, session_token=None, fields=None, language=None): params = {"placeid": place_id} if fields: + deprecated_fields = set(fields) & DEPRECATED_FIELDS + if deprecated_fields: + warnings.warn( + DEPRECATED_FIELDS_MESSAGE % str(list(deprecated_fields)), + DeprecationWarning, + ) + invalid_fields = set(fields) - PLACES_DETAIL_FIELDS if invalid_fields: - raise ValueError("Valid values for the `fields` param for " - "`place` are '%s', these given field(s) " - "are invalid: '%s'" % ( - "', '".join(PLACES_DETAIL_FIELDS), - "', '".join(invalid_fields))) + raise ValueError( + "Valid values for the `fields` param for " + "`place` are '%s', these given field(s) " + "are invalid: '%s'" + % ("', '".join(PLACES_DETAIL_FIELDS), "', '".join(invalid_fields)) + ) params["fields"] = convert.join_list(",", fields) if language: 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) @@ -403,13 +513,13 @@ def places_photo(client, photo_reference, max_width=None, max_height=None): :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.places_photo(photo_reference, max_width=100): if chunk: f.write(chunk) f.close() - ``` """ if not (max_width or max_height): @@ -425,15 +535,28 @@ 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._request("/maps/api/place/photo", params, - extract_body=lambda response: response, - requests_kwargs={"stream": True}) + response = client._request( + "/maps/api/place/photo", + params, + extract_body=lambda response: response, + requests_kwargs={"stream": True}, + ) return response.iter_content() -def places_autocomplete(client, input_text, session_token, offset=None, - location=None, radius=None, language=None, types=None, - components=None, strict_bounds=False): +def places_autocomplete( + client, + input_text, + session_token=None, + offset=None, + origin=None, + location=None, + radius=None, + language=None, + types=None, + components=None, + strict_bounds=False, +): """ Returns Place predictions given a textual search string and optional geographic bounds. @@ -451,6 +574,12 @@ def places_autocomplete(client, input_text, session_token, offset=None, 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 @@ -478,14 +607,25 @@ def places_autocomplete(client, input_text, session_token, offset=None, :rtype: list of predictions """ - return _autocomplete(client, "", input_text, session_token=session_token, - offset=offset, location=location, radius=radius, - language=language, types=types, components=components, - strict_bounds=strict_bounds) - - -def places_autocomplete_query(client, input_text, offset=None, location=None, - radius=None, language=None): + return _autocomplete( + client, + "", + input_text, + session_token=session_token, + offset=offset, + origin=origin, + location=location, + radius=radius, + language=language, + types=types, + components=components, + strict_bounds=strict_bounds, + ) + + +def places_autocomplete_query( + client, input_text, offset=None, location=None, radius=None, language=None +): """ Returns Place predictions given a textual search query, such as "pizza near New York", and optional geographic bounds. @@ -510,13 +650,31 @@ def places_autocomplete_query(client, input_text, offset=None, location=None, :rtype: list of predictions """ - return _autocomplete(client, "query", input_text, offset=offset, - location=location, radius=radius, language=language) - - -def _autocomplete(client, url_part, input_text, session_token=None, - offset=None, location=None, radius=None, language=None, - types=None, components=None, strict_bounds=False): + return _autocomplete( + client, + "query", + input_text, + offset=offset, + location=location, + radius=radius, + language=language, + ) + + +def _autocomplete( + client, + url_part, + input_text, + session_token=None, + offset=None, + origin=None, + location=None, + radius=None, + language=None, + types=None, + components=None, + strict_bounds=False, +): """ Internal handler for ``autocomplete`` and ``autocomplete_query``. See each method's docs for arg details. @@ -528,6 +686,8 @@ def _autocomplete(client, url_part, input_text, session_token=None, 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/googlemaps/test/test_client.py b/googlemaps/test/test_client.py deleted file mode 100644 index 90d83b3e..00000000 --- a/googlemaps/test/test_client.py +++ /dev/null @@ -1,308 +0,0 @@ -# -# Copyright 2014 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 client module.""" - -import time - -import responses -import requests - -import googlemaps -import googlemaps.client as _client -import googlemaps.test as _test - - -class ClientTest(_test.TestCase): - - def test_no_api_key(self): - with self.assertRaises(Exception): - client = googlemaps.Client() - client.directions("Sydney", "Melbourne") - - def test_invalid_api_key(self): - with self.assertRaises(Exception): - client = googlemaps.Client(key="Invalid key.") - client.directions("Sydney", "Melbourne") - - def test_urlencode(self): - # See GH #72. - 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, - "https://maps.googleapis.com/maps/api/geocode/json", - body='{"status":"OK","results":[]}', - status=200, - content_type="application/json") - - client = googlemaps.Client(key="AIzaasdf") - client.geocode("Sesame St.") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://maps.googleapis.com/maps/api/geocode/json?" - "key=AIzaasdf&address=Sesame+St.", - responses.calls[0].request.url) - - @responses.activate - def test_extra_params(self): - 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") - client.geocode("Sesame St.", extra_params={"foo": "bar"}) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://maps.googleapis.com/maps/api/geocode/json?" - "key=AIzaasdf&address=Sesame+St.&foo=bar", - responses.calls[0].request.url) - - def test_hmac(self): - """ - From http://en.wikipedia.org/wiki/Hash-based_message_authentication_code - - HMAC_SHA1("key", "The quick brown fox jumps over the lazy dog") - = 0xde7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 - """ - - message = "The quick brown fox jumps over the lazy dog" - key = "a2V5" # "key" -> base64 - signature = "3nybhbi3iqa8ino29wqQcBydtNk=" - - self.assertEqual(signature, _client.sign_hmac(key, message)) - - @responses.activate - def test_url_signed(self): - 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(client_id="foo", client_secret="a2V5") - client.geocode("Sesame St.") - - self.assertEqual(1, len(responses.calls)) - - # Check ordering of parameters. - self.assertIn("address=Sesame+St.&client=foo&signature", - responses.calls[0].request.url) - self.assertURLEqual("https://maps.googleapis.com/maps/api/geocode/json?" - "address=Sesame+St.&client=foo&" - "signature=fxbWUIcNPZSekVOhp2ul9LW5TpY=", - responses.calls[0].request.url) - - @responses.activate - def test_ua_sent(self): - 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") - client.geocode("Sesame St.") - - self.assertEqual(1, len(responses.calls)) - user_agent = responses.calls[0].request.headers["User-Agent"] - self.assertTrue(user_agent.startswith("GoogleGeoApiClientPython")) - - @responses.activate - def test_retry(self): - class request_callback: - def __init__(self): - self.first_req = True - - def __call__(self, req): - if self.first_req: - self.first_req = False - return (200, {}, '{"status":"OVER_QUERY_LIMIT"}') - return (200, {}, '{"status":"OK","results":[]}') - - responses.add_callback(responses.GET, - "https://maps.googleapis.com/maps/api/geocode/json", - content_type='application/json', - callback=request_callback()) - - client = googlemaps.Client(key="AIzaasdf") - client.geocode("Sesame St.") - - self.assertEqual(2, len(responses.calls)) - self.assertEqual(responses.calls[0].request.url, responses.calls[1].request.url) - - @responses.activate - def test_transport_error(self): - responses.add(responses.GET, - "https://maps.googleapis.com/maps/api/geocode/json", - status=404, - content_type='application/json') - - client = googlemaps.Client(key="AIzaasdf") - with self.assertRaises(googlemaps.exceptions.HTTPError) as e: - client.geocode("Foo") - - self.assertEqual(e.exception.status_code, 404) - - @responses.activate - def test_host_override(self): - responses.add(responses.GET, - "https://foo.com/bar", - body='{"status":"OK","results":[]}', - status=200, - content_type="application/json") - - client = googlemaps.Client(key="AIzaasdf") - client._get("/bar", {}, base_url="https://foo.com") - - self.assertEqual(1, len(responses.calls)) - - @responses.activate - def test_custom_extract(self): - def custom_extract(resp): - return resp.json() - - responses.add(responses.GET, - "https://maps.googleapis.com/bar", - body='{"error":"errormessage"}', - status=403, - content_type="application/json") - - client = googlemaps.Client(key="AIzaasdf") - b = client._get("/bar", {}, extract_body=custom_extract) - self.assertEqual(1, len(responses.calls)) - self.assertEqual("errormessage", b["error"]) - - @responses.activate - def test_retry_intermittent(self): - class request_callback: - def __init__(self): - self.first_req = True - - def __call__(self, req): - if self.first_req: - self.first_req = False - return (500, {}, 'Internal Server Error.') - return (200, {}, '{"status":"OK","results":[]}') - - responses.add_callback(responses.GET, - "https://maps.googleapis.com/maps/api/geocode/json", - content_type="application/json", - callback=request_callback()) - - client = googlemaps.Client(key="AIzaasdf") - client.geocode("Sesame St.") - - 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 - # /usage-reports#channels - with self.assertRaises(ValueError): - client = googlemaps.Client(client_id="foo", client_secret="a2V5", - channel="auieauie$? ") - - def test_auth_url_with_channel(self): - client = googlemaps.Client(key="AIzaasdf", - client_id="foo", - client_secret="a2V5", - channel="MyChannel_1") - - # Check ordering of parameters + signature. - auth_url = client._generate_auth_url("/test", - {"param": "param"}, - accepts_clientid=True) - self.assertEqual(auth_url, "/test?param=param" - "&channel=MyChannel_1" - "&client=foo" - "&signature=OH18GuQto_mEpxj99UimKskvo4k=") - - # Check if added to requests to API with accepts_clientid=False - auth_url = client._generate_auth_url("/test", - {"param": "param"}, - accepts_clientid=False) - self.assertEqual(auth_url, "/test?param=param&key=AIzaasdf") - - def test_requests_version(self): - client_args_timeout = { - "key": "AIzaasdf", - "client_id": "foo", - "client_secret": "a2V5", - "channel": "MyChannel_1", - "connect_timeout": 5, - "read_timeout": 5 - } - client_args = client_args_timeout.copy() - del client_args["connect_timeout"] - del client_args["read_timeout"] - - requests.__version__ = '2.3.0' - with self.assertRaises(NotImplementedError): - googlemaps.Client(**client_args_timeout) - googlemaps.Client(**client_args) - - requests.__version__ = '2.4.0' - googlemaps.Client(**client_args_timeout) - googlemaps.Client(**client_args) - - @responses.activate - def test_no_retry_over_query_limit(self): - responses.add(responses.GET, - "https://maps.googleapis.com/foo", - body='{"status":"OVER_QUERY_LIMIT"}', - status=200, - content_type="application/json") - - client = googlemaps.Client(key="AIzaasdf", - retry_over_query_limit=False) - - with self.assertRaises(googlemaps.exceptions.ApiError): - client._request("/foo", {}) - - self.assertEqual(1, len(responses.calls)) diff --git a/googlemaps/test/test_directions.py b/googlemaps/test/test_directions.py deleted file mode 100644 index de21dc1e..00000000 --- a/googlemaps/test/test_directions.py +++ /dev/null @@ -1,278 +0,0 @@ -# -# Copyright 2014 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 directions module.""" - -from datetime import datetime -from datetime import timedelta -import time - -import responses - -import googlemaps -import googlemaps.test as _test - - -class DirectionsTest(_test.TestCase): - - def setUp(self): - self.key = 'AIzaasdf' - self.client = googlemaps.Client(self.key) - - @responses.activate - def test_simple_directions(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - # Simplest directions request. Driving directions by default. - routes = self.client.directions("Sydney", "Melbourne") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json' - '?origin=Sydney&destination=Melbourne&key=%s' % - self.key, - responses.calls[0].request.url) - - @responses.activate - def test_complex_request(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Sydney", "Melbourne", - mode="bicycling", - avoid=["highways", "tolls", "ferries"], - units="metric", - region="us") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Sydney&avoid=highways%%7Ctolls%%7Cferries&' - 'destination=Melbourne&mode=bicycling&key=%s' - '&units=metric®ion=us' % - self.key, - responses.calls[0].request.url) - - - def test_transit_without_time(self): - # With mode of transit, we need a departure_time or an - # arrival_time specified - with self.assertRaises(googlemaps.exceptions.ApiError): - self.client.directions("Sydney Town Hall", "Parramatta, NSW", - mode="transit") - - @responses.activate - def test_transit_with_departure_time(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - now = datetime.now() - routes = self.client.directions("Sydney Town Hall", "Parramatta, NSW", - mode="transit", - traffic_model="optimistic", - departure_time=now) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?origin=' - 'Sydney+Town+Hall&key=%s&destination=Parramatta%%2C+NSW&' - 'mode=transit&departure_time=%d&traffic_model=optimistic' % - (self.key, time.mktime(now.timetuple())), - responses.calls[0].request.url) - - @responses.activate - def test_transit_with_arrival_time(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - an_hour_from_now = datetime.now() + timedelta(hours=1) - routes = self.client.directions("Sydney Town Hall", - "Parramatta, NSW", - mode="transit", - arrival_time=an_hour_from_now) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Sydney+Town+Hall&arrival_time=%d&' - 'destination=Parramatta%%2C+NSW&mode=transit&key=%s' % - (time.mktime(an_hour_from_now.timetuple()), self.key), - responses.calls[0].request.url) - - - def test_invalid_travel_mode(self): - with self.assertRaises(ValueError): - self.client.directions("48 Pirrama Road, Pyrmont, NSW", - "Sydney Town Hall", - mode="crawling") - - @responses.activate - def test_travel_mode_round_trip(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Town Hall, Sydney", - "Parramatta, NSW", - mode="bicycling") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Town+Hall%%2C+Sydney&destination=Parramatta%%2C+NSW&' - 'mode=bicycling&key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_brooklyn_to_queens_by_transit(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - now = datetime.now() - routes = self.client.directions("Brooklyn", - "Queens", - mode="transit", - departure_time=now) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Brooklyn&key=%s&destination=Queens&mode=transit&' - 'departure_time=%d' % (self.key, time.mktime(now.timetuple())), - responses.calls[0].request.url) - - @responses.activate - def test_boston_to_concord_via_charlestown_and_lexington(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Boston, MA", - "Concord, MA", - waypoints=["Charlestown, MA", - "Lexington, MA"]) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Boston%%2C+MA&destination=Concord%%2C+MA&' - 'waypoints=Charlestown%%2C+MA%%7CLexington%%2C+MA&' - 'key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_adelaide_wine_tour(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Adelaide, SA", - "Adelaide, SA", - waypoints=["Barossa Valley, SA", - "Clare, SA", - "Connawarra, SA", - "McLaren Vale, SA"], - optimize_waypoints=True) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Adelaide%%2C+SA&destination=Adelaide%%2C+SA&' - 'waypoints=optimize%%3Atrue%%7CBarossa+Valley%%2C+' - 'SA%%7CClare%%2C+SA%%7CConnawarra%%2C+SA%%7CMcLaren+' - 'Vale%%2C+SA&key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_toledo_to_madrid_in_spain(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Toledo", "Madrid", - region="es") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Toledo®ion=es&destination=Madrid&key=%s' % - self.key, - responses.calls[0].request.url) - - @responses.activate - def test_zero_results_returns_response(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"ZERO_RESULTS","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Toledo", "Madrid") - self.assertIsNotNone(routes) - self.assertEqual(0, len(routes)) - - @responses.activate - def test_language_parameter(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Toledo", "Madrid", - region="es", - language="es") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Toledo®ion=es&destination=Madrid&key=%s&' - 'language=es' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_alternatives(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/directions/json', - body='{"status":"OK","routes":[]}', - status=200, - content_type='application/json') - - routes = self.client.directions("Sydney Town Hall", - "Parramatta Town Hall", - alternatives=True) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/directions/json?' - 'origin=Sydney+Town+Hall&destination=Parramatta+Town+Hall&' - 'alternatives=true&key=%s' % self.key, - responses.calls[0].request.url) - diff --git a/googlemaps/test/test_distance_matrix.py b/googlemaps/test/test_distance_matrix.py deleted file mode 100644 index 5e929b24..00000000 --- a/googlemaps/test/test_distance_matrix.py +++ /dev/null @@ -1,150 +0,0 @@ -# -# Copyright 2014 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 distance matrix module.""" - -from datetime import datetime -import time - -import responses - -import googlemaps -import googlemaps.test as _test - - -class DistanceMatrixTest(_test.TestCase): - - def setUp(self): - self.key = 'AIzaasdf' - self.client = googlemaps.Client(self.key) - - @responses.activate - def test_basic_params(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/distancematrix/json', - body='{"status":"OK","rows":[]}', - status=200, - content_type='application/json') - - origins = ["Perth, Australia", "Sydney, Australia", - "Melbourne, Australia", "Adelaide, Australia", - "Brisbane, Australia", "Darwin, Australia", - "Hobart, Australia", "Canberra, Australia"] - destinations = ["Uluru, Australia", - "Kakadu, Australia", - "Blue Mountains, Australia", - "Bungle Bungles, Australia", - "The Pinnacles, Australia"] - - 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=Perth%%2C+Australia%%7CSydney%%2C+' - 'Australia%%7CMelbourne%%2C+Australia%%7CAdelaide%%2C+' - 'Australia%%7CBrisbane%%2C+Australia%%7CDarwin%%2C+' - 'Australia%%7CHobart%%2C+Australia%%7CCanberra%%2C+Australia&' - 'destinations=Uluru%%2C+Australia%%7CKakadu%%2C+Australia%%7C' - 'Blue+Mountains%%2C+Australia%%7CBungle+Bungles%%2C+Australia' - '%%7CThe+Pinnacles%%2C+Australia' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_mixed_params(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/distancematrix/json', - body='{"status":"OK","rows":[]}', - status=200, - content_type='application/json') - - origins = ["Bobcaygeon ON", [41.43206, -81.38992]] - destinations = [(43.012486, -83.6964149), - {"lat": 42.8863855, "lng": -78.8781627}] - - 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=Bobcaygeon+ON%%7C41.43206%%2C-81.38992&' - 'destinations=43.012486%%2C-83.696415%%7C42.886386%%2C' - '-78.878163' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_all_params(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/distancematrix/json', - body='{"status":"OK","rows":[]}', - status=200, - content_type='application/json') - - origins = ["Perth, Australia", "Sydney, Australia", - "Melbourne, Australia", "Adelaide, Australia", - "Brisbane, Australia", "Darwin, Australia", - "Hobart, Australia", "Canberra, Australia"] - destinations = ["Uluru, Australia", - "Kakadu, Australia", - "Blue Mountains, Australia", - "Bungle Bungles, Australia", - "The Pinnacles, Australia"] - - now = datetime.now() - matrix = self.client.distance_matrix(origins, destinations, - mode="driving", - language="en-AU", - avoid="tolls", - units="imperial", - departure_time=now, - traffic_model="optimistic") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/distancematrix/json?' - 'origins=Perth%%2C+Australia%%7CSydney%%2C+Australia%%7C' - 'Melbourne%%2C+Australia%%7CAdelaide%%2C+Australia%%7C' - 'Brisbane%%2C+Australia%%7CDarwin%%2C+Australia%%7CHobart%%2C+' - 'Australia%%7CCanberra%%2C+Australia&language=en-AU&' - 'avoid=tolls&mode=driving&key=%s&units=imperial&' - 'destinations=Uluru%%2C+Australia%%7CKakadu%%2C+Australia%%7C' - 'Blue+Mountains%%2C+Australia%%7CBungle+Bungles%%2C+Australia' - '%%7CThe+Pinnacles%%2C+Australia&departure_time=%d' - '&traffic_model=optimistic' % - (self.key, time.mktime(now.timetuple())), - responses.calls[0].request.url) - - - @responses.activate - def test_lang_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 = ["Vancouver BC", "Seattle"] - destinations = ["San Francisco", "Victoria BC"] - - matrix = self.client.distance_matrix(origins, destinations, - language="fr-FR", - mode="bicycling") - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/distancematrix/json?' - 'key=%s&language=fr-FR&mode=bicycling&' - 'origins=Vancouver+BC%%7CSeattle&' - 'destinations=San+Francisco%%7CVictoria+BC' % - self.key, - responses.calls[0].request.url) diff --git a/googlemaps/test/test_elevation.py b/googlemaps/test/test_elevation.py deleted file mode 100644 index 41146939..00000000 --- a/googlemaps/test/test_elevation.py +++ /dev/null @@ -1,116 +0,0 @@ -# -# Copyright 2014 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 elevation module.""" - -import datetime - -import responses - -import googlemaps -import googlemaps.test as _test - - -class ElevationTest(_test.TestCase): - - def setUp(self): - self.key = 'AIzaasdf' - self.client = googlemaps.Client(self.key) - - @responses.activate - def test_elevation_single(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/elevation/json', - body='{"status":"OK","results":[]}', - status=200, - content_type='application/json') - - results = self.client.elevation((40.714728, -73.998672)) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/elevation/json?' - 'locations=enc:abowFtzsbM&key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_elevation_single_list(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/elevation/json', - body='{"status":"OK","results":[]}', - status=200, - content_type='application/json') - - results = self.client.elevation([(40.714728, -73.998672)]) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/elevation/json?' - 'locations=enc:abowFtzsbM&key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_elevation_multiple(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/elevation/json', - body='{"status":"OK","results":[]}', - status=200, - content_type='application/json') - - locations = [(40.714728, -73.998672), (-34.397, 150.644)] - results = self.client.elevation(locations) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/elevation/json?' - 'locations=enc:abowFtzsbMhgmiMuobzi@&key=%s' % self.key, - responses.calls[0].request.url) - - def test_elevation_along_path_single(self): - with self.assertRaises(googlemaps.exceptions.ApiError): - results = self.client.elevation_along_path( - [(40.714728, -73.998672)], 5) - - @responses.activate - def test_elevation_along_path(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/elevation/json', - body='{"status":"OK","results":[]}', - status=200, - content_type='application/json') - - path = [(40.714728, -73.998672), (-34.397, 150.644)] - - results = self.client.elevation_along_path(path, 5) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/elevation/json?' - 'path=enc:abowFtzsbMhgmiMuobzi@&' - 'key=%s&samples=5' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_short_latlng(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/elevation/json', - body='{"status":"OK","results":[]}', - status=200, - content_type='application/json') - - results = self.client.elevation((40, -73)) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/elevation/json?' - 'locations=40,-73&key=%s' % self.key, - responses.calls[0].request.url) diff --git a/googlemaps/test/test_geocoding.py b/googlemaps/test/test_geocoding.py deleted file mode 100644 index 511e6d8f..00000000 --- a/googlemaps/test/test_geocoding.py +++ /dev/null @@ -1,283 +0,0 @@ -# This Python file uses the following encoding: utf-8 -# -# Copyright 2014 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 geocoding module.""" - -import datetime - -import responses - -import googlemaps -import googlemaps.test as _test - - -class GeocodingTest(_test.TestCase): - - def setUp(self): - self.key = 'AIzaasdf' - self.client = googlemaps.Client(self.key) - - @responses.activate - def test_simple_geocode(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('Sydney') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&address=Sydney' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_reverse_geocode(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.reverse_geocode((-33.8674869, 151.2069902)) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'latlng=-33.867487%%2C151.20699&key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_geocoding_the_googleplex(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('1600 Amphitheatre Parkway, ' - 'Mountain View, CA') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&address=1600+Amphitheatre+Parkway%%2C+Mountain' - '+View%%2C+CA' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_geocode_with_bounds(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('Winnetka', - bounds={'southwest': (34.172684, -118.604794), - 'northeast':(34.236144, -118.500938)}) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'bounds=34.172684%%2C-118.604794%%7C34.236144%%2C' - '-118.500938&key=%s&address=Winnetka' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_geocode_with_region_biasing(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('Toledo', region='es') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'region=es&key=%s&address=Toledo' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_geocode_with_component_filter(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('santa cruz', - components={'country': 'ES'}) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&components=country%%3AES&address=santa+cruz' % - self.key, - responses.calls[0].request.url) - - @responses.activate - def test_geocode_with_multiple_component_filters(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('Torun', - components={'administrative_area': 'TX','country': 'US'}) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&components=administrative_area%%3ATX%%7C' - 'country%%3AUS&address=Torun' % self.key, - responses.calls[0].request.url) - - - @responses.activate - def test_geocode_with_just_components(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( - components={'route': 'Annegatan', - 'administrative_area': 'Helsinki', - 'country': 'Finland'}) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&components=administrative_area%%3AHelsinki' - '%%7Ccountry%%3AFinland%%7Croute%%3AAnnegatan' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_simple_reverse_geocode(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.reverse_geocode((40.714224, -73.961452)) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'latlng=40.714224%%2C-73.961452&key=%s' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_reverse_geocode_restricted_by_type(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.reverse_geocode((40.714224, -73.961452), - location_type='ROOFTOP', - result_type='street_address') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'latlng=40.714224%%2C-73.961452&result_type=street_address&' - 'key=%s&location_type=ROOFTOP' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_reverse_geocode_multiple_location_types(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.reverse_geocode((40.714224, -73.961452), - location_type=['ROOFTOP', - 'RANGE_INTERPOLATED'], - result_type='street_address') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'latlng=40.714224%%2C-73.961452&result_type=street_address&' - 'key=%s&location_type=ROOFTOP%%7CRANGE_INTERPOLATED' % - self.key, - responses.calls[0].request.url) - - @responses.activate - def test_reverse_geocode_multiple_result_types(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.reverse_geocode((40.714224, -73.961452), - location_type='ROOFTOP', - result_type=['street_address', - 'route']) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'latlng=40.714224%%2C-73.961452&result_type=street_address' - '%%7Croute&key=%s&location_type=ROOFTOP' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_partial_match(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('Pirrama Pyrmont') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&address=Pirrama+Pyrmont' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_utf_results(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(components={'postal_code': '96766'}) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&components=postal_code%%3A96766' % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_utf8_request(self): - responses.add(responses.GET, - 'https://maps.googleapis.com/maps/api/geocode/json', - body='{"status":"OK","results":[]}', - status=200, - content_type='application/json') - - self.client.geocode(self.u('\\u4e2d\\u56fd')) # China - self.assertURLEqual( - 'https://maps.googleapis.com/maps/api/geocode/json?' - 'key=%s&address=%s' % (self.key, '%E4%B8%AD%E5%9B%BD'), - responses.calls[0].request.url) diff --git a/googlemaps/test/test_places.py b/googlemaps/test/test_places.py deleted file mode 100644 index 77d05afd..00000000 --- a/googlemaps/test/test_places.py +++ /dev/null @@ -1,199 +0,0 @@ -# This Python file uses the following encoding: utf-8 -# -# Copyright 2016 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 places module.""" - -from types import GeneratorType - -import responses - -import googlemaps -from googlemaps.places import places_autocomplete_session_token -import googlemaps.test as _test - - -class PlacesTest(_test.TestCase): - - def setUp(self): - self.key = 'AIzaasdf' - self.client = googlemaps.Client(self.key) - self.location = (-33.86746, 151.207090) - self.type = 'liquor_store' - self.language = 'en-AU' - self.region = 'AU' - self.radius = 100 - - @responses.activate - def test_places_find(self): - url = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json' - responses.add(responses.GET, url, - body='{"status": "OK", "candidates": []}', - status=200, content_type='application/json') - - self.client.find_place('restaurant', 'textquery', - fields=['geometry', 'id'], - location_bias='point:90,90', - language=self.language) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?language=en-AU&inputtype=textquery&' - 'locationbias=point:90,90&input=restaurant' - '&fields=geometry,id&key=%s' - % (url, self.key), responses.calls[0].request.url) - - with self.assertRaises(ValueError): - self.client.find_place('restaurant', 'invalid') - with self.assertRaises(ValueError): - self.client.find_place('restaurant', 'textquery', - fields=['geometry', 'invalid']) - with self.assertRaises(ValueError): - self.client.find_place('restaurant', 'textquery', - location_bias='invalid') - - @responses.activate - def test_places_text_search(self): - url = 'https://maps.googleapis.com/maps/api/place/textsearch/json' - responses.add(responses.GET, url, - body='{"status": "OK", "results": [], "html_attributions": []}', - status=200, content_type='application/json') - - self.client.places('restaurant', location=self.location, - radius=self.radius, region=self.region, language=self.language, - min_price=1, max_price=4, open_now=True, - type=self.type) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?language=en-AU&location=-33.86746%%2C151.20709&' - 'maxprice=4&minprice=1&opennow=true&query=restaurant&' - 'radius=100®ion=AU&type=liquor_store&key=%s' - % (url, self.key), responses.calls[0].request.url) - - @responses.activate - def test_places_nearby_search(self): - url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' - responses.add(responses.GET, url, - body='{"status": "OK", "results": [], "html_attributions": []}', - status=200, content_type='application/json') - - self.client.places_nearby(location=self.location, keyword='foo', - language=self.language, min_price=1, - max_price=4, name='bar', open_now=True, - rank_by='distance', type=self.type) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?keyword=foo&language=en-AU&location=-33.86746%%2C151.20709&' - 'maxprice=4&minprice=1&name=bar&opennow=true&rankby=distance&' - 'type=liquor_store&key=%s' - % (url, self.key), responses.calls[0].request.url) - - with self.assertRaises(ValueError): - self.client.places_nearby(radius=self.radius) - with self.assertRaises(ValueError): - self.client.places_nearby(self.location, rank_by="distance") - - with self.assertRaises(ValueError): - self.client.places_nearby(location=self.location, rank_by="distance", - keyword='foo', radius=self.radius) - - @responses.activate - def test_places_radar_search(self): - url = 'https://maps.googleapis.com/maps/api/place/radarsearch/json' - responses.add(responses.GET, url, - body='{"status": "OK", "results": [], "html_attributions": []}', - status=200, content_type='application/json') - - self.client.places_radar(self.location, self.radius, keyword='foo', - min_price=1, max_price=4, name='bar', - open_now=True, type=self.type) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?keyword=foo&location=-33.86746%%2C151.20709&' - 'maxprice=4&minprice=1&name=bar&opennow=true&radius=100&' - 'type=liquor_store&key=%s' - % (url, self.key), responses.calls[0].request.url) - - with self.assertRaises(ValueError): - self.client.places_radar(self.location, self.radius) - - @responses.activate - def test_place_detail(self): - url = 'https://maps.googleapis.com/maps/api/place/details/json' - responses.add(responses.GET, url, - body='{"status": "OK", "result": {}, "html_attributions": []}', - status=200, content_type='application/json') - - self.client.place('ChIJN1t_tDeuEmsRUsoyG83frY4', - fields=['geometry', 'id'], language=self.language) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?language=en-AU&placeid=ChIJN1t_tDeuEmsRUsoyG83frY4' - '&key=%s&fields=geometry,id' - % (url, self.key), responses.calls[0].request.url) - - with self.assertRaises(ValueError): - self.client.place('ChIJN1t_tDeuEmsRUsoyG83frY4', - fields=['geometry', 'invalid']) - - @responses.activate - def test_photo(self): - url = 'https://maps.googleapis.com/maps/api/place/photo' - responses.add(responses.GET, url, status=200) - - ref = 'CnRvAAAAwMpdHeWlXl-lH0vp7lez4znKPIWSWvgvZFISdKx45AwJVP1Qp37YOrH7sqHMJ8C-vBDC546decipPHchJhHZL94RcTUfPa1jWzo-rSHaTlbNtjh-N68RkcToUCuY9v2HNpo5mziqkir37WU8FJEqVBIQ4k938TI3e7bf8xq-uwDZcxoUbO_ZJzPxremiQurAYzCTwRhE_V0' - response = self.client.places_photo(ref, max_width=100) - - self.assertTrue(isinstance(response, GeneratorType)) - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?maxwidth=100&photoreference=%s&key=%s' - % (url, ref, self.key), responses.calls[0].request.url) - - @responses.activate - def test_autocomplete(self): - url = 'https://maps.googleapis.com/maps/api/place/autocomplete/json' - responses.add(responses.GET, url, - body='{"status": "OK", "predictions": []}', - status=200, content_type='application/json') - - session_token = places_autocomplete_session_token() - - self.client.places_autocomplete('Google', session_token, offset=3, - location=self.location, - radius=self.radius, - language=self.language, - types='geocode', - components={'country': 'au'}, - strict_bounds=True) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?components=country%%3Aau&input=Google&language=en-AU&' - 'location=-33.86746%%2C151.20709&offset=3&radius=100&' - 'strictbounds=true&types=geocode&key=%s&sessiontoken=%s' % - (url, self.key, session_token), responses.calls[0].request.url) - - @responses.activate - def test_autocomplete_query(self): - url = 'https://maps.googleapis.com/maps/api/place/queryautocomplete/json' - responses.add(responses.GET, url, - body='{"status": "OK", "predictions": []}', - status=200, content_type='application/json') - - self.client.places_autocomplete_query('pizza near New York') - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual('%s?input=pizza+near+New+York&key=%s' % - (url, self.key), responses.calls[0].request.url) diff --git a/googlemaps/test/test_roads.py b/googlemaps/test/test_roads.py deleted file mode 100644 index 6e11b604..00000000 --- a/googlemaps/test/test_roads.py +++ /dev/null @@ -1,137 +0,0 @@ -# -# Copyright 2015 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 roads module.""" - - -import responses - -import googlemaps -import googlemaps.test as _test - - -class RoadsTest(_test.TestCase): - - def setUp(self): - self.key = "AIzaasdf" - self.client = googlemaps.Client(self.key) - - @responses.activate - def test_snap(self): - responses.add(responses.GET, - "https://roads.googleapis.com/v1/snapToRoads", - body='{"snappedPoints":["foo"]}', - status=200, - content_type="application/json") - - results = self.client.snap_to_roads((40.714728, -73.998672)) - self.assertEqual("foo", results[0]) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://roads.googleapis.com/v1/snapToRoads?" - "path=40.714728%%2C-73.998672&key=%s" % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_nearest_roads(self): - responses.add(responses.GET, - "https://roads.googleapis.com/v1/nearestRoads", - body='{"snappedPoints":["foo"]}', - status=200, - content_type="application/json") - - results = self.client.nearest_roads((40.714728, -73.998672)) - self.assertEqual("foo", results[0]) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://roads.googleapis.com/v1/nearestRoads?" - "points=40.714728%%2C-73.998672&key=%s" % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_path(self): - responses.add(responses.GET, - "https://roads.googleapis.com/v1/speedLimits", - body='{"speedLimits":["foo"]}', - status=200, - content_type="application/json") - - results = self.client.snapped_speed_limits([(1, 2),(3, 4)]) - self.assertEqual("foo", results["speedLimits"][0]) - - self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://roads.googleapis.com/v1/speedLimits?" - "path=1%%2C2|3%%2C4" - "&key=%s" % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_speedlimits(self): - responses.add(responses.GET, - "https://roads.googleapis.com/v1/speedLimits", - body='{"speedLimits":["foo"]}', - status=200, - content_type="application/json") - - results = self.client.speed_limits("id1") - self.assertEqual("foo", results[0]) - self.assertEqual("https://roads.googleapis.com/v1/speedLimits?" - "placeId=id1&key=%s" % self.key, - responses.calls[0].request.url) - - @responses.activate - def test_speedlimits_multiple(self): - responses.add(responses.GET, - "https://roads.googleapis.com/v1/speedLimits", - body='{"speedLimits":["foo"]}', - status=200, - content_type="application/json") - - results = self.client.speed_limits(["id1", "id2", "id3"]) - self.assertEqual("foo", results[0]) - self.assertEqual("https://roads.googleapis.com/v1/speedLimits?" - "placeId=id1&placeId=id2&placeId=id3" - "&key=%s" % self.key, - responses.calls[0].request.url) - - def test_clientid_not_accepted(self): - client = googlemaps.Client(client_id="asdf", client_secret="asdf") - - with self.assertRaises(ValueError): - client.speed_limits("foo") - - @responses.activate - def test_retry(self): - class request_callback: - def __init__(self): - self.first_req = True - - def __call__(self, req): - if self.first_req: - self.first_req = False - return (500, {}, 'Internal Server Error.') - return (200, {}, '{"speedLimits":[]}') - - responses.add_callback(responses.GET, - "https://roads.googleapis.com/v1/speedLimits", - content_type="application/json", - callback=request_callback()) - - self.client.speed_limits([]) - - self.assertEqual(2, len(responses.calls)) - self.assertEqual(responses.calls[0].request.url, responses.calls[1].request.url) diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..e5b92961 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,81 @@ +# 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.7", "3.8", "3.9", "3.10"] + + +def _install_dev_packages(session): + session.install("-e", ".") + + +def _install_test_dependencies(session): + session.install("pytest") + session.install("pytest-cov") + session.install("responses") + + +def _install_doc_dependencies(session): + session.install("sphinx") + + +@nox.session(python=SUPPORTED_PY_VERSIONS) +def tests(session): + _install_dev_packages(session) + _install_test_dependencies(session) + + session.install("pytest") + session.run("pytest") + + session.notify("cover") + + +@nox.session +def cover(session): + """Coverage analysis.""" + session.install("coverage") + session.install("codecov") + session.run("coverage", "report", "--show-missing") + session.run("codecov") + session.run("coverage", "erase") + + +@nox.session(python="3.7") +def docs(session): + _install_dev_packages(session) + _install_doc_dependencies(session) + + session.run("rm", "-rf", "docs/_build", external=True) + + sphinx_args = [ + "-a", + "-E", + "-b", + "html", + "-d", + "docs/_build/doctrees", + "docs", + "docs/_build/html", + ] + + sphinx_cmd = "sphinx-build" + + session.run(sphinx_cmd, *sphinx_args) + + +@nox.session() +def distribution(session): + session.run("bash", ".github/scripts/distribution.sh", external=True) + session.run("python", "-c", "import googlemaps") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..56320971 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[tool:pytest] +addopts = -rsxX --cov=googlemaps --cov-report= + +[coverage:run] +omit = + tests/* + +[coverage:report] +exclude_lines = + pragma: no cover + def __repr__ + raise NotImplementedError \ No newline at end of file diff --git a/setup.py b/setup.py index e3bf4286..df9f32f1 100644 --- a/setup.py +++ b/setup.py @@ -1,42 +1,52 @@ -import sys +# 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 -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +requirements = ["requests>=2.20.0,<3.0"] -if sys.version_info <= (2, 4): - error = 'Requires Python Version 2.5 or above... exiting.' - print >> sys.stderr, error - sys.exit(1) +with open("README.md") as f: + readme = f.read() +with open("CHANGELOG.md") as f: + changelog = f.read() -requirements = [ - 'requests>=2.20.0,<3.0', -] -setup(name='googlemaps', - version='3.0.2', - description='Python client library for Google Maps API Web Services', - scripts=[], - url='https://github.com/googlemaps/google-maps-services-python', - packages=['googlemaps'], - license='Apache 2.0', - platforms='Posix; MacOS X; Windows', - setup_requires=requirements, - install_requires=requirements, - test_suite='googlemaps.test', - classifiers=['Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Internet', - ] - ) +setup( + name="googlemaps", + version="4.10.0", + description="Python client library for Google Maps Platform", + long_description=readme + changelog, + long_description_content_type="text/markdown", + scripts=[], + url="https://github.com/googlemaps/google-maps-services-python", + packages=["googlemaps"], + license="Apache 2.0", + platforms="Posix; MacOS X; Windows", + setup_requires=requirements, + install_requires=requirements, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Internet", + ], + python_requires='>=3.5' +) diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 5646081a..00000000 --- a/test_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -nose -responses==0.3 diff --git a/googlemaps/test/__init__.py b/tests/__init__.py similarity index 90% rename from googlemaps/test/__init__.py rename to tests/__init__.py index 38c70917..8a32b1ed 100644 --- a/googlemaps/test/__init__.py +++ b/tests/__init__.py @@ -18,14 +18,10 @@ import unittest import codecs -try: # Python 3 - from urllib.parse import urlparse, parse_qsl -except ImportError: # Python 2 - from urlparse import urlparse, parse_qsl +from urllib.parse import urlparse, parse_qsl class TestCase(unittest.TestCase): - def assertURLEqual(self, first, second, msg=None): """Check that two arguments are equivalent URLs. Ignores the order of query arguments. 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 new file mode 100644 index 00000000..4f01e397 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,432 @@ +# +# Copyright 2014 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 client module.""" + +import time + +import responses +import requests +import uuid + +import googlemaps +import googlemaps.client as _client +from . import TestCase +from googlemaps.client import _X_GOOG_MAPS_EXPERIENCE_ID + + +class ClientTest(TestCase): + def test_no_api_key(self): + with self.assertRaises(Exception): + client = googlemaps.Client() + client.directions("Sydney", "Melbourne") + + def test_invalid_api_key(self): + with self.assertRaises(Exception): + client = googlemaps.Client(key="Invalid key.") + client.directions("Sydney", "Melbourne") + + def test_urlencode(self): + # See GH #72. + 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, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + client = googlemaps.Client(key="AIzaasdf") + client.geocode("Sesame St.") + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=AIzaasdf&address=Sesame+St.", + responses.calls[0].request.url, + ) + + @responses.activate + def test_extra_params(self): + 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") + client.geocode("Sesame St.", extra_params={"foo": "bar"}) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=AIzaasdf&address=Sesame+St.&foo=bar", + responses.calls[0].request.url, + ) + + def test_hmac(self): + """ + From http://en.wikipedia.org/wiki/Hash-based_message_authentication_code + + HMAC_SHA1("key", "The quick brown fox jumps over the lazy dog") + = 0xde7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 + """ + + message = "The quick brown fox jumps over the lazy dog" + key = "a2V5" # "key" -> base64 + signature = "3nybhbi3iqa8ino29wqQcBydtNk=" + + self.assertEqual(signature, _client.sign_hmac(key, message)) + + @responses.activate + def test_url_signed(self): + 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(client_id="foo", client_secret="a2V5") + client.geocode("Sesame St.") + + self.assertEqual(1, len(responses.calls)) + + # Check ordering of parameters. + self.assertIn( + "address=Sesame+St.&client=foo&signature", responses.calls[0].request.url + ) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "address=Sesame+St.&client=foo&" + "signature=fxbWUIcNPZSekVOhp2ul9LW5TpY=", + responses.calls[0].request.url, + ) + + @responses.activate + def test_ua_sent(self): + 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") + client.geocode("Sesame St.") + + self.assertEqual(1, len(responses.calls)) + user_agent = responses.calls[0].request.headers["User-Agent"] + self.assertTrue(user_agent.startswith("GoogleGeoApiClientPython")) + + @responses.activate + def test_retry(self): + class request_callback: + def __init__(self): + self.first_req = True + + def __call__(self, req): + if self.first_req: + self.first_req = False + return (200, {}, '{"status":"OVER_QUERY_LIMIT"}') + return (200, {}, '{"status":"OK","results":[]}') + + responses.add_callback( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + content_type="application/json", + callback=request_callback(), + ) + + client = googlemaps.Client(key="AIzaasdf") + client.geocode("Sesame St.") + + self.assertEqual(2, len(responses.calls)) + self.assertEqual(responses.calls[0].request.url, responses.calls[1].request.url) + + @responses.activate + def test_transport_error(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + status=404, + content_type="application/json", + ) + + client = googlemaps.Client(key="AIzaasdf") + with self.assertRaises(googlemaps.exceptions.HTTPError) as e: + client.geocode("Foo") + + self.assertEqual(e.exception.status_code, 404) + + @responses.activate + def test_host_override_on_init(self): + responses.add( + responses.GET, + "https://foo.com/bar", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + client = googlemaps.Client(key="AIzaasdf", base_url="https://foo.com") + client._get("/bar", {}) + + self.assertEqual(1, len(responses.calls)) + + @responses.activate + def test_host_override_per_request(self): + responses.add( + responses.GET, + "https://foo.com/bar", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + client = googlemaps.Client(key="AIzaasdf") + client._get("/bar", {}, base_url="https://foo.com") + + self.assertEqual(1, len(responses.calls)) + + @responses.activate + def test_custom_extract(self): + def custom_extract(resp): + return resp.json() + + responses.add( + responses.GET, + "https://maps.googleapis.com/bar", + body='{"error":"errormessage"}', + status=403, + content_type="application/json", + ) + + client = googlemaps.Client(key="AIzaasdf") + b = client._get("/bar", {}, extract_body=custom_extract) + self.assertEqual(1, len(responses.calls)) + self.assertEqual("errormessage", b["error"]) + + @responses.activate + def test_retry_intermittent(self): + class request_callback: + def __init__(self): + self.first_req = True + + def __call__(self, req): + if self.first_req: + self.first_req = False + return (500, {}, "Internal Server Error.") + return (200, {}, '{"status":"OK","results":[]}') + + responses.add_callback( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + content_type="application/json", + callback=request_callback(), + ) + + client = googlemaps.Client(key="AIzaasdf") + client.geocode("Sesame St.") + + self.assertEqual(2, len(responses.calls)) + + def test_invalid_channel(self): + # Cf. limitations here: + # https://developers.google.com/maps/premium/reports + # /usage-reports#channels + with self.assertRaises(ValueError): + client = googlemaps.Client( + client_id="foo", client_secret="a2V5", channel="auieauie$? " + ) + + def test_auth_url_with_channel(self): + client = googlemaps.Client( + key="AIzaasdf", client_id="foo", client_secret="a2V5", channel="MyChannel_1" + ) + + # Check ordering of parameters + signature. + auth_url = client._generate_auth_url( + "/test", {"param": "param"}, accepts_clientid=True + ) + self.assertEqual( + auth_url, + "/test?param=param" + "&channel=MyChannel_1" + "&client=foo" + "&signature=OH18GuQto_mEpxj99UimKskvo4k=", + ) + + # Check if added to requests to API with accepts_clientid=False + auth_url = client._generate_auth_url( + "/test", {"param": "param"}, accepts_clientid=False + ) + self.assertEqual(auth_url, "/test?param=param&key=AIzaasdf") + + def test_requests_version(self): + client_args_timeout = { + "key": "AIzaasdf", + "client_id": "foo", + "client_secret": "a2V5", + "channel": "MyChannel_1", + "connect_timeout": 5, + "read_timeout": 5, + } + client_args = client_args_timeout.copy() + del client_args["connect_timeout"] + del client_args["read_timeout"] + + requests.__version__ = "2.3.0" + with self.assertRaises(NotImplementedError): + googlemaps.Client(**client_args_timeout) + googlemaps.Client(**client_args) + + requests.__version__ = "2.4.0" + googlemaps.Client(**client_args_timeout) + googlemaps.Client(**client_args) + + def test_single_experience_id(self): + experience_id1 = "Exp1" + client = googlemaps.Client(key="AIzaasdf", experience_id=experience_id1) + self.assertEqual(experience_id1, client.get_experience_id()) + + experience_id2 = "Exp2" + client.set_experience_id(experience_id2) + self.assertEqual(experience_id2, client.get_experience_id()) + + def test_multiple_experience_id(self): + client = googlemaps.Client(key="AIzaasdf") + + experience_id1 = "Exp1" + experience_id2 = "Exp2" + client.set_experience_id(experience_id1, experience_id2) + + result = "%s,%s" % (experience_id1, experience_id2) + self.assertEqual(result, client.get_experience_id()) + + def test_no_experience_id(self): + client = googlemaps.Client(key="AIzaasdf") + self.assertIsNone(client.get_experience_id()) + + def test_clearing_experience_id(self): + client = googlemaps.Client(key="AIzaasdf") + client.set_experience_id("ExpId") + client.clear_experience_id() + self.assertIsNone(client.get_experience_id()) + + def test_experience_id_sample(self): + # [START maps_experience_id] + experience_id = str(uuid.uuid4()) + + # instantiate client with experience id + client = googlemaps.Client(key="AIza-Maps-API-Key", experience_id=experience_id) + + # clear the current experience id + client.clear_experience_id() + + # set a new experience id + other_experience_id = str(uuid.uuid4()) + client.set_experience_id(experience_id, other_experience_id) + + # make API request, the client will set the header + # X-GOOG-MAPS-EXPERIENCE-ID: experience_id,other_experience_id + + # get current experience id + ids = client.get_experience_id() + # [END maps_experience_id] + + result = "%s,%s" % (experience_id, other_experience_id) + self.assertEqual(result, ids) + + @responses.activate + def _perform_mock_request(self, experience_id=None): + # Mock response + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + # Perform network call + client = googlemaps.Client(key="AIzaasdf") + client.set_experience_id(experience_id) + client.geocode("Sesame St.") + return responses.calls[0].request + + def test_experience_id_in_header(self): + experience_id = "Exp1" + request = self._perform_mock_request(experience_id) + header_value = request.headers[_X_GOOG_MAPS_EXPERIENCE_ID] + self.assertEqual(experience_id, header_value) + + def test_experience_id_no_in_header(self): + request = self._perform_mock_request() + self.assertIsNone(request.headers.get(_X_GOOG_MAPS_EXPERIENCE_ID)) + + @responses.activate + def test_no_retry_over_query_limit(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/foo", + body='{"status":"OVER_QUERY_LIMIT"}', + status=200, + content_type="application/json", + ) + + client = googlemaps.Client(key="AIzaasdf", retry_over_query_limit=False) + + with self.assertRaises(googlemaps.exceptions.ApiError): + client._request("/foo", {}) + + self.assertEqual(1, len(responses.calls)) diff --git a/googlemaps/test/test_convert.py b/tests/test_convert.py similarity index 62% rename from googlemaps/test/test_convert.py rename to tests/test_convert.py index ed08c84b..39546aee 100644 --- a/googlemaps/test/test_convert.py +++ b/tests/test_convert.py @@ -19,12 +19,12 @@ import datetime import unittest +import pytest from googlemaps import convert class ConvertTest(unittest.TestCase): - def test_latlng(self): expected = "1,2" ll = {"lat": 1, "lng": 2} @@ -112,32 +112,42 @@ def test_bounds(self): with self.assertRaises(TypeError): convert.bounds("test") + def test_size(self): + self.assertEqual("1x1", convert.size(1)) + + self.assertEqual("2x3", convert.size((2, 3))) + + with self.assertRaises(TypeError): + convert.size("test") + def test_polyline_decode(self): - syd_mel_route = ("rvumEis{y[`NsfA~tAbF`bEj^h{@{KlfA~eA~`AbmEghAt~D|e@j" - "lRpO~yH_\\v}LjbBh~FdvCxu@`nCplDbcBf_B|wBhIfhCnqEb~D~" - "jCn_EngApdEtoBbfClf@t_CzcCpoEr_Gz_DxmAphDjjBxqCviEf}" - "B|pEvsEzbE~qGfpExjBlqCx}BvmLb`FbrQdpEvkAbjDllD|uDldD" - "j`Ef|AzcEx_Gtm@vuI~xArwD`dArlFnhEzmHjtC~eDluAfkC|eAd" - "hGpJh}N_mArrDlr@h|HzjDbsAvy@~~EdTxpJje@jlEltBboDjJdv" - "KyZpzExrAxpHfg@pmJg[tgJuqBnlIarAh}DbN`hCeOf_IbxA~uFt" - "|A|xEt_ArmBcN|sB|h@b_DjOzbJ{RlxCcfAp~AahAbqG~Gr}AerA" - "`dCwlCbaFo]twKt{@bsG|}A~fDlvBvz@tw@rpD_r@rqB{PvbHek@" - "vsHlh@ptNtm@fkD[~xFeEbyKnjDdyDbbBtuA|~Br|Gx_AfxCt}Cj" - "nHv`Ew\\lnBdrBfqBraD|{BldBxpG|]jqC`mArcBv]rdAxgBzdEb" - "{InaBzyC}AzaEaIvrCzcAzsCtfD~qGoPfeEh]h`BxiB`e@`kBxfA" - "v^pyA`}BhkCdoCtrC~bCxhCbgEplKrk@tiAteBwAxbCwuAnnCc]b" - "{FjrDdjGhhGzfCrlDruBzSrnGhvDhcFzw@n{@zxAf}Fd{IzaDnbD" - "joAjqJjfDlbIlzAraBxrB}K~`GpuD~`BjmDhkBp{@r_AxCrnAjrC" - "x`AzrBj{B|r@~qBbdAjtDnvCtNzpHxeApyC|GlfM`fHtMvqLjuEt" - "lDvoFbnCt|@xmAvqBkGreFm~@hlHw|AltC}NtkGvhBfaJ|~@riAx" - "uC~gErwCttCzjAdmGuF`iFv`AxsJftD|nDr_QtbMz_DheAf~Buy@" - "rlC`i@d_CljC`gBr|H|nAf_Fh{G|mE~kAhgKviEpaQnu@zwAlrA`" - "G~gFnvItz@j{Cng@j{D{]`tEftCdcIsPz{DddE~}PlnE|dJnzG`e" - "G`mF|aJdqDvoAwWjzHv`H`wOtjGzeXhhBlxErfCf{BtsCjpEjtD|" - "}Aja@xnAbdDt|ErMrdFh{CzgAnlCnr@`wEM~mE`bA`uD|MlwKxmB" - "vuFlhB|sN`_@fvBp`CxhCt_@loDsS|eDlmChgFlqCbjCxk@vbGxm" - "CjbMba@rpBaoClcCk_DhgEzYdzBl\\vsA_JfGztAbShkGtEhlDzh" - "C~w@hnB{e@yF}`D`_Ayx@~vGqn@l}CafC") + syd_mel_route = ( + "rvumEis{y[`NsfA~tAbF`bEj^h{@{KlfA~eA~`AbmEghAt~D|e@j" + "lRpO~yH_\\v}LjbBh~FdvCxu@`nCplDbcBf_B|wBhIfhCnqEb~D~" + "jCn_EngApdEtoBbfClf@t_CzcCpoEr_Gz_DxmAphDjjBxqCviEf}" + "B|pEvsEzbE~qGfpExjBlqCx}BvmLb`FbrQdpEvkAbjDllD|uDldD" + "j`Ef|AzcEx_Gtm@vuI~xArwD`dArlFnhEzmHjtC~eDluAfkC|eAd" + "hGpJh}N_mArrDlr@h|HzjDbsAvy@~~EdTxpJje@jlEltBboDjJdv" + "KyZpzExrAxpHfg@pmJg[tgJuqBnlIarAh}DbN`hCeOf_IbxA~uFt" + "|A|xEt_ArmBcN|sB|h@b_DjOzbJ{RlxCcfAp~AahAbqG~Gr}AerA" + "`dCwlCbaFo]twKt{@bsG|}A~fDlvBvz@tw@rpD_r@rqB{PvbHek@" + "vsHlh@ptNtm@fkD[~xFeEbyKnjDdyDbbBtuA|~Br|Gx_AfxCt}Cj" + "nHv`Ew\\lnBdrBfqBraD|{BldBxpG|]jqC`mArcBv]rdAxgBzdEb" + "{InaBzyC}AzaEaIvrCzcAzsCtfD~qGoPfeEh]h`BxiB`e@`kBxfA" + "v^pyA`}BhkCdoCtrC~bCxhCbgEplKrk@tiAteBwAxbCwuAnnCc]b" + "{FjrDdjGhhGzfCrlDruBzSrnGhvDhcFzw@n{@zxAf}Fd{IzaDnbD" + "joAjqJjfDlbIlzAraBxrB}K~`GpuD~`BjmDhkBp{@r_AxCrnAjrC" + "x`AzrBj{B|r@~qBbdAjtDnvCtNzpHxeApyC|GlfM`fHtMvqLjuEt" + "lDvoFbnCt|@xmAvqBkGreFm~@hlHw|AltC}NtkGvhBfaJ|~@riAx" + "uC~gErwCttCzjAdmGuF`iFv`AxsJftD|nDr_QtbMz_DheAf~Buy@" + "rlC`i@d_CljC`gBr|H|nAf_Fh{G|mE~kAhgKviEpaQnu@zwAlrA`" + "G~gFnvItz@j{Cng@j{D{]`tEftCdcIsPz{DddE~}PlnE|dJnzG`e" + "G`mF|aJdqDvoAwWjzHv`H`wOtjGzeXhhBlxErfCf{BtsCjpEjtD|" + "}Aja@xnAbdDt|ErMrdFh{CzgAnlCnr@`wEM~mE`bA`uD|MlwKxmB" + "vuFlhB|sN`_@fvBp`CxhCt_@loDsS|eDlmChgFlqCbjCxk@vbGxm" + "CjbMba@rpBaoClcCk_DhgEzYdzBl\\vsA_JfGztAbShkGtEhlDzh" + "C~w@hnB{e@yF}`D`_Ayx@~vGqn@l}CafC" + ) points = convert.decode_polyline(syd_mel_route) self.assertAlmostEqual(-33.86746, points[0]["lat"]) @@ -146,9 +156,26 @@ def test_polyline_decode(self): self.assertAlmostEqual(144.963180, points[-1]["lng"]) def test_polyline_round_trip(self): - test_polyline = ("gcneIpgxzRcDnBoBlEHzKjBbHlG`@`IkDxIi" - "KhKoMaLwTwHeIqHuAyGXeB~Ew@fFjAtIzExF") + test_polyline = ( + "gcneIpgxzRcDnBoBlEHzKjBbHlG`@`IkDxIi" + "KhKoMaLwTwHeIqHuAyGXeB~Ew@fFjAtIzExF" + ) points = convert.decode_polyline(test_polyline) actual_polyline = convert.encode_polyline(points) self.assertEqual(test_polyline, actual_polyline) + + +@pytest.mark.parametrize( + "value, expected", + [ + (40, "40"), + (40.0, "40"), + (40.1, "40.1"), + (40.00000001, "40.00000001"), + (40.000000009, "40.00000001"), + (40.000000001, "40"), + ], +) +def test_format_float(value, expected): + assert convert.format_float(value) == expected diff --git a/tests/test_directions.py b/tests/test_directions.py new file mode 100644 index 00000000..5a3c477a --- /dev/null +++ b/tests/test_directions.py @@ -0,0 +1,325 @@ +# +# Copyright 2014 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 directions module.""" + +from datetime import datetime +from datetime import timedelta +import time + +import responses + +import googlemaps +from . import TestCase + + +class DirectionsTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_simple_directions(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + # Simplest directions request. Driving directions by default. + routes = self.client.directions("Sydney", "Melbourne") + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json" + "?origin=Sydney&destination=Melbourne&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_complex_request(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions( + "Sydney", + "Melbourne", + mode="bicycling", + avoid=["highways", "tolls", "ferries"], + units="metric", + region="us", + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Sydney&avoid=highways%%7Ctolls%%7Cferries&" + "destination=Melbourne&mode=bicycling&key=%s" + "&units=metric®ion=us" % self.key, + responses.calls[0].request.url, + ) + + def test_transit_without_time(self): + # With mode of transit, we need a departure_time or an + # arrival_time specified + with self.assertRaises(googlemaps.exceptions.ApiError): + self.client.directions( + "Sydney Town Hall", "Parramatta, NSW", mode="transit" + ) + + @responses.activate + def test_transit_with_departure_time(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + now = datetime.now() + routes = self.client.directions( + "Sydney Town Hall", + "Parramatta, NSW", + mode="transit", + traffic_model="optimistic", + departure_time=now, + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?origin=" + "Sydney+Town+Hall&key=%s&destination=Parramatta%%2C+NSW&" + "mode=transit&departure_time=%d&traffic_model=optimistic" + % (self.key, time.mktime(now.timetuple())), + responses.calls[0].request.url, + ) + + @responses.activate + def test_transit_with_arrival_time(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + an_hour_from_now = datetime.now() + timedelta(hours=1) + routes = self.client.directions( + "Sydney Town Hall", + "Parramatta, NSW", + mode="transit", + arrival_time=an_hour_from_now, + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Sydney+Town+Hall&arrival_time=%d&" + "destination=Parramatta%%2C+NSW&mode=transit&key=%s" + % (time.mktime(an_hour_from_now.timetuple()), self.key), + responses.calls[0].request.url, + ) + + def test_invalid_travel_mode(self): + with self.assertRaises(ValueError): + self.client.directions( + "48 Pirrama Road, Pyrmont, NSW", "Sydney Town Hall", mode="crawling" + ) + + @responses.activate + def test_travel_mode_round_trip(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions( + "Town Hall, Sydney", "Parramatta, NSW", mode="bicycling" + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Town+Hall%%2C+Sydney&destination=Parramatta%%2C+NSW&" + "mode=bicycling&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_brooklyn_to_queens_by_transit(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + now = datetime.now() + routes = self.client.directions( + "Brooklyn", "Queens", mode="transit", departure_time=now + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Brooklyn&key=%s&destination=Queens&mode=transit&" + "departure_time=%d" % (self.key, time.mktime(now.timetuple())), + responses.calls[0].request.url, + ) + + @responses.activate + def test_boston_to_concord_via_charlestown_and_lexington(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions( + "Boston, MA", "Concord, MA", waypoints=["Charlestown, MA", "Lexington, MA"] + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Boston%%2C+MA&destination=Concord%%2C+MA&" + "waypoints=Charlestown%%2C+MA%%7CLexington%%2C+MA&" + "key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_adelaide_wine_tour(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions( + "Adelaide, SA", + "Adelaide, SA", + waypoints=[ + "Barossa Valley, SA", + "Clare, SA", + "Connawarra, SA", + "McLaren Vale, SA", + ], + optimize_waypoints=True, + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Adelaide%%2C+SA&destination=Adelaide%%2C+SA&" + "waypoints=optimize%%3Atrue%%7CBarossa+Valley%%2C+" + "SA%%7CClare%%2C+SA%%7CConnawarra%%2C+SA%%7CMcLaren+" + "Vale%%2C+SA&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_toledo_to_madrid_in_spain(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions("Toledo", "Madrid", region="es") + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Toledo®ion=es&destination=Madrid&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_zero_results_returns_response(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"ZERO_RESULTS","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions("Toledo", "Madrid") + self.assertIsNotNone(routes) + self.assertEqual(0, len(routes)) + + @responses.activate + def test_language_parameter(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions("Toledo", "Madrid", region="es", language="es") + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Toledo®ion=es&destination=Madrid&key=%s&" + "language=es" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_alternatives(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/directions/json", + body='{"status":"OK","routes":[]}', + status=200, + content_type="application/json", + ) + + routes = self.client.directions( + "Sydney Town Hall", "Parramatta Town Hall", alternatives=True + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=Sydney+Town+Hall&destination=Parramatta+Town+Hall&" + "alternatives=true&key=%s" % self.key, + responses.calls[0].request.url, + ) diff --git a/tests/test_distance_matrix.py b/tests/test_distance_matrix.py new file mode 100644 index 00000000..6946782e --- /dev/null +++ b/tests/test_distance_matrix.py @@ -0,0 +1,218 @@ +# +# Copyright 2014 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 distance matrix module.""" + +from datetime import datetime +import time + +import responses + +import googlemaps +from . import TestCase + + +class DistanceMatrixTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_basic_params(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/distancematrix/json", + body='{"status":"OK","rows":[]}', + status=200, + content_type="application/json", + ) + + origins = [ + "Perth, Australia", + "Sydney, Australia", + "Melbourne, Australia", + "Adelaide, Australia", + "Brisbane, Australia", + "Darwin, Australia", + "Hobart, Australia", + "Canberra, Australia", + ] + destinations = [ + "Uluru, Australia", + "Kakadu, Australia", + "Blue Mountains, Australia", + "Bungle Bungles, Australia", + "The Pinnacles, Australia", + ] + + 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=Perth%%2C+Australia%%7CSydney%%2C+" + "Australia%%7CMelbourne%%2C+Australia%%7CAdelaide%%2C+" + "Australia%%7CBrisbane%%2C+Australia%%7CDarwin%%2C+" + "Australia%%7CHobart%%2C+Australia%%7CCanberra%%2C+Australia&" + "destinations=Uluru%%2C+Australia%%7CKakadu%%2C+Australia%%7C" + "Blue+Mountains%%2C+Australia%%7CBungle+Bungles%%2C+Australia" + "%%7CThe+Pinnacles%%2C+Australia" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_mixed_params(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/distancematrix/json", + body='{"status":"OK","rows":[]}', + status=200, + content_type="application/json", + ) + + origins = [ + "Bobcaygeon ON", [41.43206, -81.38992], + "place_id:ChIJ7cv00DwsDogRAMDACa2m4K8" + ] + destinations = [ + (43.012486, -83.6964149), + {"lat": 42.8863855, "lng": -78.8781627}, + ] + + 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=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, + ) + + @responses.activate + def test_all_params(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/distancematrix/json", + body='{"status":"OK","rows":[]}', + status=200, + content_type="application/json", + ) + + origins = [ + "Perth, Australia", + "Sydney, Australia", + "Melbourne, Australia", + "Adelaide, Australia", + "Brisbane, Australia", + "Darwin, Australia", + "Hobart, Australia", + "Canberra, Australia", + ] + destinations = [ + "Uluru, Australia", + "Kakadu, Australia", + "Blue Mountains, Australia", + "Bungle Bungles, Australia", + "The Pinnacles, Australia", + ] + + now = datetime.now() + matrix = self.client.distance_matrix( + origins, + destinations, + mode="driving", + language="en-AU", + avoid="tolls", + units="imperial", + departure_time=now, + traffic_model="optimistic", + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/distancematrix/json?" + "origins=Perth%%2C+Australia%%7CSydney%%2C+Australia%%7C" + "Melbourne%%2C+Australia%%7CAdelaide%%2C+Australia%%7C" + "Brisbane%%2C+Australia%%7CDarwin%%2C+Australia%%7CHobart%%2C+" + "Australia%%7CCanberra%%2C+Australia&language=en-AU&" + "avoid=tolls&mode=driving&key=%s&units=imperial&" + "destinations=Uluru%%2C+Australia%%7CKakadu%%2C+Australia%%7C" + "Blue+Mountains%%2C+Australia%%7CBungle+Bungles%%2C+Australia" + "%%7CThe+Pinnacles%%2C+Australia&departure_time=%d" + "&traffic_model=optimistic" % (self.key, time.mktime(now.timetuple())), + responses.calls[0].request.url, + ) + + @responses.activate + def test_lang_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 = ["Vancouver BC", "Seattle"] + destinations = ["San Francisco", "Victoria BC"] + + matrix = self.client.distance_matrix( + origins, destinations, language="fr-FR", mode="bicycling" + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/distancematrix/json?" + "key=%s&language=fr-FR&mode=bicycling&" + "origins=Vancouver+BC%%7CSeattle&" + "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_elevation.py b/tests/test_elevation.py new file mode 100644 index 00000000..165f95b3 --- /dev/null +++ b/tests/test_elevation.py @@ -0,0 +1,134 @@ +# +# Copyright 2014 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 elevation module.""" + +import datetime + +import responses + +import googlemaps +from . import TestCase + + +class ElevationTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_elevation_single(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/elevation/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + results = self.client.elevation((40.714728, -73.998672)) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/elevation/json?" + "locations=enc:abowFtzsbM&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_elevation_single_list(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/elevation/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + results = self.client.elevation([(40.714728, -73.998672)]) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/elevation/json?" + "locations=enc:abowFtzsbM&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_elevation_multiple(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/elevation/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + locations = [(40.714728, -73.998672), (-34.397, 150.644)] + results = self.client.elevation(locations) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/elevation/json?" + "locations=enc:abowFtzsbMhgmiMuobzi@&key=%s" % self.key, + responses.calls[0].request.url, + ) + + def test_elevation_along_path_single(self): + with self.assertRaises(googlemaps.exceptions.ApiError): + results = self.client.elevation_along_path([(40.714728, -73.998672)], 5) + + @responses.activate + def test_elevation_along_path(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/elevation/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + path = [(40.714728, -73.998672), (-34.397, 150.644)] + + results = self.client.elevation_along_path(path, 5) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/elevation/json?" + "path=enc:abowFtzsbMhgmiMuobzi@&" + "key=%s&samples=5" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_short_latlng(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/elevation/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + results = self.client.elevation((40, -73)) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/elevation/json?" + "locations=40,-73&key=%s" % self.key, + responses.calls[0].request.url, + ) diff --git a/tests/test_geocoding.py b/tests/test_geocoding.py new file mode 100644 index 00000000..8734c8b8 --- /dev/null +++ b/tests/test_geocoding.py @@ -0,0 +1,389 @@ +# This Python file uses the following encoding: utf-8 +# +# Copyright 2014 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 geocoding module.""" + +import datetime + +import responses + +import googlemaps +from . import TestCase + + +class GeocodingTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_simple_geocode(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("Sydney").get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&address=Sydney" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_reverse_geocode(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.reverse_geocode((-33.8674869, 151.2069902)).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "latlng=-33.8674869,151.2069902&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_geocoding_the_googleplex(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("1600 Amphitheatre Parkway, " "Mountain View, CA").get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&address=1600+Amphitheatre+Parkway%%2C+Mountain" + "+View%%2C+CA" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_geocode_with_bounds(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( + "Winnetka", + bounds={ + "southwest": (34.172684, -118.604794), + "northeast": (34.236144, -118.500938), + }, + ).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "bounds=34.172684%%2C-118.604794%%7C34.236144%%2C" + "-118.500938&key=%s&address=Winnetka" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_geocode_with_region_biasing(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("Toledo", region="es").get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "region=es&key=%s&address=Toledo" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_geocode_with_component_filter(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("santa cruz", components={"country": "ES"}).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&components=country%%3AES&address=santa+cruz" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_geocode_with_multiple_component_filters(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( + "Torun", components={"administrative_area": "TX", "country": "US"} + ).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&components=administrative_area%%3ATX%%7C" + "country%%3AUS&address=Torun" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_geocode_with_just_components(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( + components={ + "route": "Annegatan", + "administrative_area": "Helsinki", + "country": "Finland", + } + ).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&components=administrative_area%%3AHelsinki" + "%%7Ccountry%%3AFinland%%7Croute%%3AAnnegatan" % self.key, + 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( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + results = self.client.reverse_geocode((40.714224, -73.961452)).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "latlng=40.714224%%2C-73.961452&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_reverse_geocode_restricted_by_type(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.reverse_geocode( + (40.714224, -73.961452), + location_type="ROOFTOP", + result_type="street_address", + ).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "latlng=40.714224%%2C-73.961452&result_type=street_address&" + "key=%s&location_type=ROOFTOP" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_reverse_geocode_multiple_location_types(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.reverse_geocode( + (40.714224, -73.961452), + location_type=["ROOFTOP", "RANGE_INTERPOLATED"], + result_type="street_address", + ).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "latlng=40.714224%%2C-73.961452&result_type=street_address&" + "key=%s&location_type=ROOFTOP%%7CRANGE_INTERPOLATED" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_reverse_geocode_multiple_result_types(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.reverse_geocode( + (40.714224, -73.961452), + location_type="ROOFTOP", + result_type=["street_address", "route"], + ).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "latlng=40.714224%%2C-73.961452&result_type=street_address" + "%%7Croute&key=%s&location_type=ROOFTOP" % self.key, + 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( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + results = self.client.geocode("Pirrama Pyrmont").get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&address=Pirrama+Pyrmont" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_utf_results(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(components={"postal_code": "96766"}).get("results", []) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/geocode/json?" + "key=%s&components=postal_code%%3A96766" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_utf8_request(self): + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/geocode/json", + body='{"status":"OK","results":[]}', + status=200, + content_type="application/json", + ) + + 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"), + responses.calls[0].request.url, + ) diff --git a/googlemaps/test/test_geolocation.py b/tests/test_geolocation.py similarity index 63% rename from googlemaps/test/test_geolocation.py rename to tests/test_geolocation.py index 64b6f36a..8eeb2cbc 100644 --- a/googlemaps/test/test_geolocation.py +++ b/tests/test_geolocation.py @@ -21,25 +21,28 @@ import responses import googlemaps -import googlemaps.test as _test +from . import TestCase -class GeolocationTest(_test.TestCase): - +class GeolocationTest(TestCase): def setUp(self): - self.key = 'AIzaasdf' + 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') + 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) + self.assertURLEqual( + "https://www.googleapis.com/geolocation/v1/geolocate?" "key=%s" % self.key, + responses.calls[0].request.url, + ) diff --git a/tests/test_maps.py b/tests/test_maps.py new file mode 100644 index 00000000..8db6298f --- /dev/null +++ b/tests/test_maps.py @@ -0,0 +1,129 @@ +# +# Copyright 2020 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 maps module.""" + +from types import GeneratorType + +import responses + +import googlemaps +from . import TestCase + +from googlemaps.maps import StaticMapMarker +from googlemaps.maps import StaticMapPath + + +class MapsTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_static_map_marker(self): + marker = StaticMapMarker( + locations=[{"lat": -33.867486, "lng": 151.206990}, "Sydney"], + size="small", + color="blue", + label="S", + ) + + self.assertEqual( + "size:small|color:blue|label:S|" "-33.867486,151.20699|Sydney", str(marker) + ) + + with self.assertRaises(ValueError): + StaticMapMarker(locations=["Sydney"], label="XS") + + self.assertEqual("label:1|Sydney", str(StaticMapMarker(locations=["Sydney"], label="1"))) + + @responses.activate + def test_static_map_path(self): + path = StaticMapPath( + points=[{"lat": -33.867486, "lng": 151.206990}, "Sydney"], + weight=5, + color="red", + geodesic=True, + fillcolor="Red", + ) + + self.assertEqual( + "weight:5|color:red|fillcolor:Red|" + "geodesic:True|" + "-33.867486,151.20699|Sydney", + str(path), + ) + + @responses.activate + def test_download(self): + url = "https://maps.googleapis.com/maps/api/staticmap" + responses.add(responses.GET, url, status=200) + + path = StaticMapPath( + points=[(62.107733, -145.541936), "Delta+Junction,AK"], + weight=5, + color="red", + ) + + m1 = StaticMapMarker( + locations=[(62.107733, -145.541936)], color="blue", label="S" + ) + + m2 = StaticMapMarker( + locations=["Delta+Junction,AK"], size="tiny", color="green" + ) + + m3 = StaticMapMarker( + locations=["Tok,AK"], size="mid", color="0xFFFF00", label="C" + ) + + response = self.client.static_map( + size=(400, 400), + zoom=6, + center=(63.259591, -144.667969), + maptype="hybrid", + format="png", + scale=2, + visible=["Tok,AK"], + path=path, + markers=[m1, m2, m3], + ) + + self.assertTrue(isinstance(response, GeneratorType)) + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "%s?center=63.259591%%2C-144.667969&format=png&maptype=hybrid&" + "markers=color%%3Ablue%%7Clabel%%3AS%%7C62.107733%%2C-145.541936&" + "markers=size%%3Atiny%%7Ccolor%%3Agreen%%7CDelta%%2BJunction%%2CAK&" + "markers=size%%3Amid%%7Ccolor%%3A0xFFFF00%%7Clabel%%3AC%%7CTok%%2CAK&" + "path=weight%%3A5%%7Ccolor%%3Ared%%7C62.107733%%2C-145.541936%%7CDelta%%2BJunction%%2CAK&" + "scale=2&size=400x400&visible=Tok%%2CAK&zoom=6&key=%s" % (url, self.key), + responses.calls[0].request.url, + ) + + with self.assertRaises(ValueError): + self.client.static_map(size=(400, 400)) + + with self.assertRaises(ValueError): + self.client.static_map( + size=(400, 400), center=(63.259591, -144.667969), zoom=6, format="test" + ) + + with self.assertRaises(ValueError): + self.client.static_map( + size=(400, 400), center=(63.259591, -144.667969), zoom=6, maptype="test" + ) diff --git a/tests/test_places.py b/tests/test_places.py new file mode 100644 index 00000000..32f03d16 --- /dev/null +++ b/tests/test_places.py @@ -0,0 +1,255 @@ +# This Python file uses the following encoding: utf-8 +# +# Copyright 2016 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 places module.""" + +import uuid + +from types import GeneratorType + +import responses + +import googlemaps +from . import TestCase + + +class PlacesTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + self.location = (-33.86746, 151.207090) + self.type = "liquor_store" + self.language = "en-AU" + self.region = "AU" + self.radius = 100 + + @responses.activate + def test_places_find(self): + url = "https://maps.googleapis.com/maps/api/place/findplacefromtext/json" + responses.add( + responses.GET, + url, + body='{"status": "OK", "candidates": []}', + status=200, + content_type="application/json", + ) + + self.client.find_place( + "restaurant", + "textquery", + fields=["business_status", "geometry/location", "place_id"], + location_bias="point:90,90", + language=self.language, + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "%s?language=en-AU&inputtype=textquery&" + "locationbias=point:90,90&input=restaurant" + "&fields=business_status,geometry/location,place_id&key=%s" + % (url, self.key), + responses.calls[0].request.url, + ) + + with self.assertRaises(ValueError): + self.client.find_place("restaurant", "invalid") + with self.assertRaises(ValueError): + self.client.find_place( + "restaurant", "textquery", fields=["geometry", "invalid"] + ) + with self.assertRaises(ValueError): + self.client.find_place("restaurant", "textquery", location_bias="invalid") + + @responses.activate + def test_places_text_search(self): + url = "https://maps.googleapis.com/maps/api/place/textsearch/json" + responses.add( + responses.GET, + url, + body='{"status": "OK", "results": [], "html_attributions": []}', + status=200, + content_type="application/json", + ) + + self.client.places( + "restaurant", + location=self.location, + radius=self.radius, + region=self.region, + language=self.language, + min_price=1, + max_price=4, + open_now=True, + type=self.type, + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "%s?language=en-AU&location=-33.86746%%2C151.20709&" + "maxprice=4&minprice=1&opennow=true&query=restaurant&" + "radius=100®ion=AU&type=liquor_store&key=%s" % (url, self.key), + responses.calls[0].request.url, + ) + + @responses.activate + def test_places_nearby_search(self): + url = "https://maps.googleapis.com/maps/api/place/nearbysearch/json" + responses.add( + responses.GET, + url, + body='{"status": "OK", "results": [], "html_attributions": []}', + status=200, + content_type="application/json", + ) + + self.client.places_nearby( + location=self.location, + keyword="foo", + language=self.language, + min_price=1, + max_price=4, + name="bar", + open_now=True, + rank_by="distance", + type=self.type, + ) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "%s?keyword=foo&language=en-AU&location=-33.86746%%2C151.20709&" + "maxprice=4&minprice=1&name=bar&opennow=true&rankby=distance&" + "type=liquor_store&key=%s" % (url, self.key), + responses.calls[0].request.url, + ) + + with self.assertRaises(ValueError): + self.client.places_nearby(radius=self.radius) + with self.assertRaises(ValueError): + self.client.places_nearby(self.location, rank_by="distance") + + with self.assertRaises(ValueError): + self.client.places_nearby( + location=self.location, + rank_by="distance", + keyword="foo", + radius=self.radius, + ) + + @responses.activate + def test_place_detail(self): + url = "https://maps.googleapis.com/maps/api/place/details/json" + responses.add( + responses.GET, + url, + body='{"status": "OK", "result": {}, "html_attributions": []}', + status=200, + content_type="application/json", + ) + + self.client.place( + "ChIJN1t_tDeuEmsRUsoyG83frY4", + 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" + "&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, + ) + + with self.assertRaises(ValueError): + self.client.place( + "ChIJN1t_tDeuEmsRUsoyG83frY4", fields=["geometry", "invalid"] + ) + + @responses.activate + def test_photo(self): + url = "https://maps.googleapis.com/maps/api/place/photo" + responses.add(responses.GET, url, status=200) + + ref = "CnRvAAAAwMpdHeWlXl-lH0vp7lez4znKPIWSWvgvZFISdKx45AwJVP1Qp37YOrH7sqHMJ8C-vBDC546decipPHchJhHZL94RcTUfPa1jWzo-rSHaTlbNtjh-N68RkcToUCuY9v2HNpo5mziqkir37WU8FJEqVBIQ4k938TI3e7bf8xq-uwDZcxoUbO_ZJzPxremiQurAYzCTwRhE_V0" + response = self.client.places_photo(ref, max_width=100) + + self.assertTrue(isinstance(response, GeneratorType)) + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "%s?maxwidth=100&photoreference=%s&key=%s" % (url, ref, self.key), + responses.calls[0].request.url, + ) + + @responses.activate + def test_autocomplete(self): + url = "https://maps.googleapis.com/maps/api/place/autocomplete/json" + responses.add( + responses.GET, + url, + body='{"status": "OK", "predictions": []}', + status=200, + content_type="application/json", + ) + + session_token = uuid.uuid4().hex + + self.client.places_autocomplete( + "Google", + session_token=session_token, + offset=3, + origin=self.location, + location=self.location, + radius=self.radius, + language=self.language, + types="geocode", + components={"country": "au"}, + strict_bounds=True, + ) + + 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), + responses.calls[0].request.url, + ) + + @responses.activate + def test_autocomplete_query(self): + url = "https://maps.googleapis.com/maps/api/place/queryautocomplete/json" + responses.add( + responses.GET, + url, + body='{"status": "OK", "predictions": []}', + status=200, + content_type="application/json", + ) + + self.client.places_autocomplete_query("pizza near New York") + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "%s?input=pizza+near+New+York&key=%s" % (url, self.key), + responses.calls[0].request.url, + ) diff --git a/tests/test_roads.py b/tests/test_roads.py new file mode 100644 index 00000000..bfea2bf7 --- /dev/null +++ b/tests/test_roads.py @@ -0,0 +1,158 @@ +# +# Copyright 2015 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 roads module.""" + + +import responses + +import googlemaps +from . import TestCase + + +class RoadsTest(TestCase): + def setUp(self): + self.key = "AIzaasdf" + self.client = googlemaps.Client(self.key) + + @responses.activate + def test_snap(self): + responses.add( + responses.GET, + "https://roads.googleapis.com/v1/snapToRoads", + body='{"snappedPoints":["foo"]}', + status=200, + content_type="application/json", + ) + + results = self.client.snap_to_roads((40.714728, -73.998672)) + self.assertEqual("foo", results[0]) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://roads.googleapis.com/v1/snapToRoads?" + "path=40.714728%%2C-73.998672&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_nearest_roads(self): + responses.add( + responses.GET, + "https://roads.googleapis.com/v1/nearestRoads", + body='{"snappedPoints":["foo"]}', + status=200, + content_type="application/json", + ) + + results = self.client.nearest_roads((40.714728, -73.998672)) + self.assertEqual("foo", results[0]) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://roads.googleapis.com/v1/nearestRoads?" + "points=40.714728%%2C-73.998672&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_path(self): + responses.add( + responses.GET, + "https://roads.googleapis.com/v1/speedLimits", + body='{"speedLimits":["foo"]}', + status=200, + content_type="application/json", + ) + + results = self.client.snapped_speed_limits([(1, 2), (3, 4)]) + self.assertEqual("foo", results["speedLimits"][0]) + + self.assertEqual(1, len(responses.calls)) + self.assertURLEqual( + "https://roads.googleapis.com/v1/speedLimits?" + "path=1%%2C2|3%%2C4" + "&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_speedlimits(self): + responses.add( + responses.GET, + "https://roads.googleapis.com/v1/speedLimits", + body='{"speedLimits":["foo"]}', + status=200, + content_type="application/json", + ) + + results = self.client.speed_limits("id1") + self.assertEqual("foo", results[0]) + self.assertEqual( + "https://roads.googleapis.com/v1/speedLimits?" + "placeId=id1&key=%s" % self.key, + responses.calls[0].request.url, + ) + + @responses.activate + def test_speedlimits_multiple(self): + responses.add( + responses.GET, + "https://roads.googleapis.com/v1/speedLimits", + body='{"speedLimits":["foo"]}', + status=200, + content_type="application/json", + ) + + results = self.client.speed_limits(["id1", "id2", "id3"]) + self.assertEqual("foo", results[0]) + self.assertEqual( + "https://roads.googleapis.com/v1/speedLimits?" + "placeId=id1&placeId=id2&placeId=id3" + "&key=%s" % self.key, + responses.calls[0].request.url, + ) + + def test_clientid_not_accepted(self): + client = googlemaps.Client(client_id="asdf", client_secret="asdf") + + with self.assertRaises(ValueError): + client.speed_limits("foo") + + @responses.activate + def test_retry(self): + class request_callback: + def __init__(self): + self.first_req = True + + def __call__(self, req): + if self.first_req: + self.first_req = False + return (500, {}, "Internal Server Error.") + return (200, {}, '{"speedLimits":[]}') + + responses.add_callback( + responses.GET, + "https://roads.googleapis.com/v1/speedLimits", + content_type="application/json", + callback=request_callback(), + ) + + self.client.speed_limits([]) + + self.assertEqual(2, len(responses.calls)) + self.assertEqual(responses.calls[0].request.url, responses.calls[1].request.url) diff --git a/googlemaps/test/test_timezone.py b/tests/test_timezone.py similarity index 55% rename from googlemaps/test/test_timezone.py rename to tests/test_timezone.py index 23daeca3..a1d7394e 100644 --- a/googlemaps/test/test_timezone.py +++ b/tests/test_timezone.py @@ -21,58 +21,62 @@ import datetime import responses -import mock - +from unittest import mock import googlemaps -import googlemaps.test as _test - +from . import TestCase -class TimezoneTest(_test.TestCase): +class TimezoneTest(TestCase): def setUp(self): self.key = "AIzaasdf" self.client = googlemaps.Client(self.key) @responses.activate def test_los_angeles(self): - responses.add(responses.GET, - "https://maps.googleapis.com/maps/api/timezone/json", - body='{"status":"OK"}', - status=200, - content_type="application/json") + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/timezone/json", + body='{"status":"OK"}', + status=200, + content_type="application/json", + ) ts = 1331766000 timezone = self.client.timezone((39.603481, -119.682251), ts) self.assertIsNotNone(timezone) self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://maps.googleapis.com/maps/api/timezone/json" - "?location=39.603481,-119.682251×tamp=%d" - "&key=%s" % - (ts, self.key), - responses.calls[0].request.url) - - class MockDatetime(object): + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/timezone/json" + "?location=39.603481,-119.682251×tamp=%d" + "&key=%s" % (ts, self.key), + responses.calls[0].request.url, + ) + class MockDatetime: def now(self): return datetime.datetime.fromtimestamp(1608) + utcnow = now @responses.activate @mock.patch("googlemaps.timezone.datetime", MockDatetime()) def test_los_angeles_with_no_timestamp(self): - responses.add(responses.GET, - "https://maps.googleapis.com/maps/api/timezone/json", - body='{"status":"OK"}', - status=200, - content_type="application/json") + responses.add( + responses.GET, + "https://maps.googleapis.com/maps/api/timezone/json", + body='{"status":"OK"}', + status=200, + content_type="application/json", + ) timezone = self.client.timezone((39.603481, -119.682251)) self.assertIsNotNone(timezone) self.assertEqual(1, len(responses.calls)) - self.assertURLEqual("https://maps.googleapis.com/maps/api/timezone/json" - "?location=39.603481,-119.682251×tamp=%d" - "&key=%s" % - (1608, self.key), - responses.calls[0].request.url) + self.assertURLEqual( + "https://maps.googleapis.com/maps/api/timezone/json" + "?location=39.603481,-119.682251×tamp=%d" + "&key=%s" % (1608, self.key), + responses.calls[0].request.url, + ) 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 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 77ca5b7e..00000000 --- a/tox.ini +++ /dev/null @@ -1,16 +0,0 @@ -[tox] -envlist = - py27,py32,py34,py35,py36,docs - -[testenv] -commands = - nosetests googlemaps/test -deps = -rtest_requirements.txt - -[testenv:docs] -basepython = - python2.7 -commands = - sphinx-build -a -E -b html -d docs/_build/doctrees docs docs/_build/html -deps = - Sphinx