├── test ├── __init__.py ├── extra │ ├── __init__.py │ └── rate_limiter.py ├── adapters │ ├── __init__.py │ └── retry_after.py ├── geocoders │ ├── azure.py │ ├── geocodeearth.py │ ├── pickpoint.py │ ├── openmapquest.py │ ├── banfrance.py │ ├── smartystreets.py │ ├── databc.py │ ├── tomtom.py │ ├── yandex.py │ ├── mapquest.py │ ├── pelias.py │ ├── geolake.py │ ├── geocodio.py │ ├── photon.py │ ├── algolia.py │ ├── mapbox.py │ ├── bing.py │ ├── baidu.py │ ├── maptiler.py │ ├── arcgis.py │ ├── what3words.py │ ├── opencage.py │ ├── geonames.py │ └── util.py ├── test_exc.py ├── test_init.py ├── test_format.py ├── selfsigned_ca.pem ├── test_timezone.py └── test_location.py ├── docs ├── _static │ ├── favicon.ico │ └── logo-wide.png └── Makefile ├── geopy ├── extra │ └── __init__.py ├── compat.py ├── geocoders │ ├── googlev3.py │ ├── osm.py │ ├── geocodeearth.py │ ├── azure.py │ ├── pickpoint.py │ ├── openmapquest.py │ ├── smartystreets.py │ ├── databc.py │ ├── geolake.py │ ├── banfrance.py │ ├── mapbox.py │ ├── maptiler.py │ ├── mapquest.py │ ├── yandex.py │ └── pelias.py ├── util.py ├── __init__.py ├── __main__.py ├── timezone.py ├── format.py ├── exc.py ├── units.py └── location.py ├── MANIFEST.in ├── .gitignore ├── setup.cfg ├── .readthedocs.yml ├── pytest.ini ├── .coveragerc ├── .editorconfig ├── tox.ini ├── LICENSE ├── RELEASE.md ├── .github ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ └── ci.yml ├── Makefile ├── setup.py ├── README.rst ├── AUTHORS └── CONTRIBUTING.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/extra/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitnr/geopy/master/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitnr/geopy/master/docs/_static/logo-wide.png -------------------------------------------------------------------------------- /geopy/extra/__init__.py: -------------------------------------------------------------------------------- 1 | # Extra modules are intentionally not exported here, to avoid 2 | # them being always imported even when they are not needed. 3 | -------------------------------------------------------------------------------- /geopy/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | # >=3.7 3 | from asyncio import current_task 4 | except ImportError: 5 | from asyncio import Task 6 | current_task = Task.current_task 7 | del Task 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include test * 2 | global-exclude *.py[co] 3 | global-exclude .DS_Store 4 | global-exclude __pycache__ 5 | include AUTHORS 6 | include LICENSE 7 | include README.rst 8 | include pytest.ini 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .coverage 3 | .venv* 4 | venv 5 | build 6 | dist 7 | *.egg 8 | *.egg-info 9 | docs/_build 10 | .test_keys 11 | .tox 12 | *.config 13 | .DS_Store 14 | .idea/ 15 | .python-version 16 | .pytest_cache 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-complexity = 24 3 | max-line-length = 90 4 | 5 | [isort] 6 | ; https://github.com/timothycrosley/isort#multi-line-output-modes 7 | combine_as_imports = True 8 | force_grid_wrap = 0 9 | include_trailing_comma = True 10 | known_first_party = test 11 | line_length = 88 12 | multi_line_output = 3 13 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | python: 7 | version: 3 8 | install: 9 | - method: pip 10 | path: . 11 | extra_requirements: 12 | - dev-docs 13 | - method: setuptools 14 | path: . 15 | system_packages: true 16 | 17 | formats: all 18 | -------------------------------------------------------------------------------- /geopy/geocoders/googlev3.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from geopy.geocoders.google import GoogleV3 4 | 5 | __all__ = ("GoogleV3",) 6 | 7 | warnings.warn( 8 | "`geopy.geocoders.googlev3` module is deprecated. " 9 | "Use `geopy.geocoders.google` instead. " 10 | "In geopy 3 this module will be removed.", 11 | DeprecationWarning, 12 | stacklevel=2, 13 | ) 14 | -------------------------------------------------------------------------------- /geopy/geocoders/osm.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from geopy.geocoders.nominatim import Nominatim 4 | 5 | __all__ = ("Nominatim",) 6 | 7 | warnings.warn( 8 | "`geopy.geocoders.osm` module is deprecated. " 9 | "Use `geopy.geocoders.nominatim` instead. " 10 | "In geopy 3 this module will be removed.", 11 | DeprecationWarning, 12 | stacklevel=2, 13 | ) 14 | -------------------------------------------------------------------------------- /test/geocoders/azure.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import AzureMaps 2 | from test.geocoders.tomtom import BaseTestTomTom 3 | from test.geocoders.util import env 4 | 5 | 6 | class TestAzureMaps(BaseTestTomTom): 7 | 8 | @classmethod 9 | def make_geocoder(cls, **kwargs): 10 | return AzureMaps(env['AZURE_SUBSCRIPTION_KEY'], timeout=3, 11 | **kwargs) 12 | -------------------------------------------------------------------------------- /test/geocoders/geocodeearth.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import GeocodeEarth 2 | from test.geocoders.pelias import BaseTestPelias 3 | from test.geocoders.util import env 4 | 5 | 6 | class TestGeocodeEarth(BaseTestPelias): 7 | 8 | @classmethod 9 | def make_geocoder(cls, **kwargs): 10 | return GeocodeEarth(env['GEOCODEEARTH_KEY'], 11 | **kwargs) 12 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = 3 | test/test_*.py 4 | test/adapters/*.py 5 | test/extra/*.py 6 | test/geocoders/*.py 7 | 8 | ; Bodies of HTTP errors are logged with INFO level 9 | log_level = INFO 10 | 11 | ; Show warnings. Similar to `python -Wd`. 12 | filterwarnings = d 13 | 14 | ; Show skip reasons 15 | ; Print shorter tracebacks 16 | addopts = -ra --tb=short 17 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | geopy 4 | test 5 | 6 | [report] 7 | show_missing = True 8 | ; Remember that tests run from forks and locally don't have access to 9 | ; geocoders' credentials, that means that a significant amount of the code 10 | ; (which requires these creds) cannot be tested. So coverage in CI differs 11 | ; significantly between the builds for forks and for master. 12 | fail_under = 50 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.py] 13 | indent_size = 4 14 | # Docstrings and comments should have max_line_length = 80. 15 | max_line_length = 90 16 | 17 | [*.{md,rst}] 18 | trim_trailing_whitespace = false 19 | max_line_length = 80 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /test/test_exc.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import pytest 4 | 5 | import geopy.exc 6 | 7 | error_classes = sorted( 8 | [ 9 | v 10 | for v in (getattr(geopy.exc, name) for name in dir(geopy.exc)) 11 | if inspect.isclass(v) and issubclass(v, geopy.exc.GeopyError) 12 | ], 13 | key=lambda cls: cls.__name__, 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize("error_cls", error_classes) 18 | def test_init(error_cls): 19 | with pytest.raises(error_cls): 20 | raise error_cls("dummy") 21 | -------------------------------------------------------------------------------- /test/test_init.py: -------------------------------------------------------------------------------- 1 | from distutils.version import LooseVersion 2 | 3 | from geopy import __version__, __version_info__, get_version 4 | 5 | 6 | def test_version(): 7 | assert isinstance(__version__, str) and __version__ 8 | 9 | 10 | def test_version_info(): 11 | expected_version_info = tuple(LooseVersion(__version__).version) 12 | assert expected_version_info == __version_info__ 13 | 14 | 15 | def test_get_version(): 16 | version = get_version() 17 | assert isinstance(version, str) and version == __version__ 18 | -------------------------------------------------------------------------------- /geopy/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from decimal import Decimal 3 | 4 | NUMBER_TYPES = (int, float, Decimal) 5 | 6 | __version__ = "2.2.0" 7 | __version_info__ = (2, 2, 0) 8 | 9 | logger = logging.getLogger('geopy') 10 | 11 | 12 | def pairwise(seq): 13 | """ 14 | Pair an iterable, e.g., (1, 2, 3, 4) -> ((1, 2), (2, 3), (3, 4)) 15 | """ 16 | for i in range(0, len(seq) - 1): 17 | yield (seq[i], seq[i + 1]) 18 | 19 | 20 | def join_filter(sep, seq, pred=bool): 21 | """ 22 | Join with a filter. 23 | """ 24 | return sep.join([str(i) for i in seq if pred(i)]) 25 | 26 | 27 | def get_version(): 28 | return __version__ 29 | -------------------------------------------------------------------------------- /test/geocoders/pickpoint.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from geopy.geocoders import PickPoint 4 | from test.geocoders.nominatim import BaseTestNominatim 5 | from test.geocoders.util import env 6 | 7 | 8 | class TestPickPoint(BaseTestNominatim): 9 | 10 | @classmethod 11 | def make_geocoder(cls, **kwargs): 12 | return PickPoint(api_key=env['PICKPOINT_KEY'], 13 | timeout=3, **kwargs) 14 | 15 | async def test_no_nominatim_user_agent_warning(self): 16 | with warnings.catch_warnings(record=True) as w: 17 | warnings.simplefilter('always') 18 | PickPoint(api_key=env['PICKPOINT_KEY']) 19 | assert 0 == len(w) 20 | -------------------------------------------------------------------------------- /test/geocoders/openmapquest.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import OpenMapQuest 2 | from test.geocoders.nominatim import BaseTestNominatim 3 | from test.geocoders.util import env 4 | 5 | 6 | class TestUnitOpenMapQuest: 7 | 8 | def test_user_agent_custom(self): 9 | geocoder = OpenMapQuest( 10 | api_key='DUMMYKEY1234', 11 | user_agent='my_user_agent/1.0' 12 | ) 13 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 14 | 15 | 16 | class TestOpenMapQuest(BaseTestNominatim): 17 | 18 | @classmethod 19 | def make_geocoder(cls, **kwargs): 20 | return OpenMapQuest(api_key=env['OPENMAPQUEST_APIKEY'], 21 | timeout=3, **kwargs) 22 | -------------------------------------------------------------------------------- /test/adapters/retry_after.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from geopy.adapters import get_retry_after 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "headers, expected_retry_after", 12 | [ 13 | ({}, None), 14 | ({"retry-after": "42"}, 42), 15 | ({"retry-after": "Wed, 21 Oct 2015 07:28:44 GMT"}, 43), 16 | ({"retry-after": "Wed, 21 Oct 2015 06:28:44 GMT"}, 0), 17 | ({"retry-after": "Wed"}, None), 18 | ], 19 | ) 20 | def test_get_retry_after(headers, expected_retry_after): 21 | current_time = datetime.datetime( 22 | 2015, 10, 21, 7, 28, 1, tzinfo=datetime.timezone.utc 23 | ).timestamp() 24 | with patch.object(time, "time", return_value=current_time): 25 | assert expected_retry_after == get_retry_after(headers) 26 | -------------------------------------------------------------------------------- /test/test_format.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.format import format_degrees 4 | from geopy.point import Point 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "input, expected", 9 | [ 10 | (("12", "30", 0), "12 30' 0\""), 11 | (("12", "30", "30"), "12 30' 30\""), 12 | (("12", "30", 30.4), "12 30' 30.4\""), 13 | ], 14 | ) 15 | def test_format_simple(input, expected): 16 | assert format_degrees(Point.parse_degrees(*input)) == expected 17 | 18 | 19 | # These tests currently fail, because conversion to degrees (as float) 20 | # causes loss in precision (mostly because of divisions by 3): 21 | @pytest.mark.xfail 22 | @pytest.mark.parametrize( 23 | "input, expected", 24 | [ 25 | (("13", "20", 0), "13 20' 0\""), # actual: `13 20' 2.13163e-12"` 26 | (("-13", "19", 0), "-13 19' 0\""), # actual: `-13 18' 60"` 27 | ], 28 | ) 29 | def test_format_float_precision(input, expected): 30 | assert format_degrees(Point.parse_degrees(*input)) == expected 31 | -------------------------------------------------------------------------------- /geopy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | geopy is a Python client for several popular geocoding web services. 3 | 4 | geopy makes it easy for Python developers to locate the coordinates of 5 | addresses, cities, countries, and landmarks across the globe using third-party 6 | geocoders and other data sources. 7 | 8 | geopy is tested against CPython (versions 3.5, 3.6, 3.7, 3.8, 3.9) 9 | and PyPy3. geopy 1.x line also supported CPython 2.7, 3.4 and PyPy2. 10 | """ 11 | 12 | from geopy.geocoders import * # noqa 13 | from geopy.location import Location # noqa 14 | from geopy.point import Point # noqa 15 | from geopy.timezone import Timezone # noqa 16 | from geopy.util import __version__, __version_info__, get_version # noqa 17 | 18 | # geopy.geocoders.options must not be importable as `geopy.options`, 19 | # because that is ambiguous (which options are that). 20 | del options # noqa 21 | 22 | # `__all__` is intentionally not defined in order to not duplicate 23 | # the same list of geocoders as in `geopy.geocoders` package. 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{35,36,37,38,39,310,py3}, 4 | py35{-async,-noextras}, 5 | lint, 6 | rst, 7 | 8 | [testenv] 9 | extras = 10 | dev-test 11 | aiohttp 12 | requests 13 | timezone 14 | passenv = * 15 | whitelist_externals = make 16 | commands = make {env:GEOPY_TOX_TARGET:test} 17 | 18 | [testenv:py35-async] 19 | # Run a single job with asyncio adapter: 20 | # (not the whole matrix, to avoid spending extra quota) 21 | setenv = GEOPY_TEST_ADAPTER=geopy.adapters.AioHTTPAdapter 22 | 23 | [testenv:py35-noextras] 24 | # Ensure `pip install geopy` without any non-test extras doesn't break. 25 | extras = 26 | dev-test 27 | 28 | [gh-actions] 29 | python = 30 | 3.5: py35 31 | 3.6: py36 32 | 3.7: py37 33 | 3.8: py38 34 | 3.9: py39 35 | 3.10: py310 36 | pypy3: pypy3 37 | 38 | [testenv:lint] 39 | basepython = python3 40 | extras = 41 | dev-lint 42 | usedevelop = True 43 | commands = make lint 44 | 45 | [testenv:rst] 46 | basepython = python3 47 | extras = 48 | dev-docs 49 | commands = make rst_check 50 | -------------------------------------------------------------------------------- /test/selfsigned_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICyDCCAbACCQC8JTfjHqqtszANBgkqhkiG9w0BAQsFADAlMQ4wDAYDVQQKDAVn 3 | ZW9weTETMBEGA1UECwwKdW5pdCB0ZXN0czAgFw0xODA1MDkwODM5MDFaGA8yMTE4 4 | MDQxNTA4MzkwMVowJTEOMAwGA1UECgwFZ2VvcHkxEzARBgNVBAsMCnVuaXQgdGVz 5 | dHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtSG8S9I3M2C+eFcON 6 | fOafU4zCd9lQH4W3nY8L9GvvFBLNUXinWRCs4Oh3qaz2X3zlhOfFOXqFJQl653aN 7 | GfPUwWIRVa2q0jmuF7AJStjHx1aymnO28QJPK3sRzA2bIFD/v9iyLn4fGk6RkYnV 8 | Kf10uKpBDVuKvfTTPkujrYcqKelS03i0+ciC196LCPZdnWCEwjI/IiFtMfDIm2O6 9 | jOGTyKyMef74xl7sHbSrICwxkPM4e+DGbdkTpSKbWRz0QLoA+ygDuEtp0I+MAMr6 10 | 1gaM7tttPdHluLjP7mB/vjn5qghs83qoVO29jap6gC/HK1bPqrerD5tOhArARtJF 11 | 9F+XAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF/Bvn6l9GetXh6XZC+zgM4MC/kk 12 | SADd/nvo31TmkJgsBFa28CbGPDTfC/jy3ni3IW2sYfTaV9QDqfs5qQVADYqWVTIT 13 | Qz5x05UK5wxyWqlbd9wfGcK75ebYFqEaXXwvtuGKyHGwiTeAyUjSAvuwIdjnOe6H 14 | uYmte7LvzCzeuElm7hNTCZjI34sna3byX/Umq4Z2IlU6+IVE4M5kpEo1zyR6a3bw 15 | nQQkGgulYc5hqw1mIUtXTvafrwYbdk6GdOYzQh1gN6IRdeWVhdKrjrQj5rBgpN47 16 | 4LFJhRyczmapflWX4MsROUJBN/UdsMvmfHMalS/rnFqo6YvQlDqBN0dIkCs= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2018 geopy authors (see AUTHORS) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | ## Prepare 4 | 5 | 1. Ensure that all issues in the target milestone are 6 | closed: https://github.com/geopy/geopy/milestones 7 | 1. Add missing `.. versionadded`/`.. versionchanged` directives 8 | where appropriate. 9 | 1. `make authors`, review the changes, `git commit -m "Pull up AUTHORS"` 10 | 1. Write changelog in docs. 11 | 1. Push the changes, ensure that the CI build is green and all tests pass. 12 | 13 | ## Release 14 | 15 | 1. Change version in `geopy/util.py`, commit and push. 16 | 1. `make release`. When prompted add the same changelog to the git tag, 17 | but in markdown instead of rst. 18 | 1. Create a new release for the pushed tag at https://github.com/geopy/geopy/releases 19 | 1. Close the milestone, add a new one. 20 | 21 | ## Check 22 | 23 | 1. Ensure that the uploaded version works in a clean environment 24 | (e.g. `docker run -it --rm python:3.7 bash`) 25 | and execute the examples in README. 26 | 1. Ensure that RTD builds have passed and the `stable` version has updated: 27 | https://readthedocs.org/projects/geopy/builds/ 28 | 1. Ensure that the CI build for the tag is green. 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true # default 2 | contact_links: 3 | - name: 🙏 Stack Overflow 4 | url: https://stackoverflow.com/questions/tagged/geopy 5 | about: | 6 | If you have a usage question or an error rather than a specific 7 | bug report, Stack Overflow might be the better place for that. 8 | There's a somewhat active community here so you will probably get 9 | a solution quicker. And also there is a large amount of already 10 | resolved questions which can help too! Just remember to put the `geopy` 11 | tag if you'd decide to open a question. 12 | - name: 💬 Discussions 13 | url: https://github.com/geopy/geopy/discussions 14 | about: | 15 | GitHub Discussions is 16 | a good place to start if Stack Overflow didn't help or you have 17 | some idea or a feature request you'd like to bring up, or if you 18 | just have trouble and not sure you're doing everything right. 19 | Solutions and helpful snippets/patterns are also very welcome here. 20 | - name: 📝 Contributing policy 21 | url: https://github.com/geopy/geopy/blob/master/CONTRIBUTING.md 22 | about: | 23 | See CONTRIBUTING.md for more details on how issues and PRs 24 | are handled in geopy. 25 | -------------------------------------------------------------------------------- /test/geocoders/banfrance.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import BANFrance 2 | from test.geocoders.util import BaseTestGeocoder 3 | 4 | 5 | class TestBANFrance(BaseTestGeocoder): 6 | 7 | @classmethod 8 | def make_geocoder(cls, **kwargs): 9 | return BANFrance(timeout=10, **kwargs) 10 | 11 | async def test_user_agent_custom(self): 12 | geocoder = BANFrance( 13 | user_agent='my_user_agent/1.0' 14 | ) 15 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 16 | 17 | async def test_geocode_with_address(self): 18 | location = await self.geocode_run( 19 | {"query": "Camp des Landes, 41200 VILLEFRANCHE-SUR-CHER"}, 20 | {"latitude": 47.293048, "longitude": 1.718985}, 21 | ) 22 | assert "Camp des Landes" in location.address 23 | 24 | async def test_reverse(self): 25 | location = await self.reverse_run( 26 | {"query": "48.154587,3.221237"}, 27 | {"latitude": 48.154587, "longitude": 3.221237}, 28 | ) 29 | assert "Collemiers" in location.address 30 | 31 | async def test_geocode_limit(self): 32 | result = await self.geocode_run( 33 | {"query": "8 bd du port", "limit": 2, "exactly_one": False}, 34 | {} 35 | ) 36 | assert 2 >= len(result) 37 | -------------------------------------------------------------------------------- /test/geocoders/smartystreets.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import geopy.geocoders 4 | from geopy.geocoders import LiveAddress 5 | from test.geocoders.util import BaseTestGeocoder, env 6 | 7 | 8 | class TestUnitLiveAddress: 9 | dummy_id = 'DUMMY12345' 10 | dummy_token = 'DUMMY67890' 11 | 12 | def test_user_agent_custom(self): 13 | geocoder = LiveAddress( 14 | auth_id=self.dummy_id, 15 | auth_token=self.dummy_token, 16 | user_agent='my_user_agent/1.0' 17 | ) 18 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 19 | 20 | @patch.object(geopy.geocoders.options, 'default_scheme', 'http') 21 | def test_default_scheme_is_ignored(self): 22 | geocoder = LiveAddress(auth_id=self.dummy_id, auth_token=self.dummy_token) 23 | assert geocoder.scheme == 'https' 24 | 25 | 26 | class TestLiveAddress(BaseTestGeocoder): 27 | 28 | @classmethod 29 | def make_geocoder(cls, **kwargs): 30 | return LiveAddress( 31 | auth_id=env['LIVESTREETS_AUTH_ID'], 32 | auth_token=env['LIVESTREETS_AUTH_TOKEN'], 33 | **kwargs 34 | ) 35 | 36 | async def test_geocode(self): 37 | await self.geocode_run( 38 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 39 | {"latitude": 41.890, "longitude": -87.624}, 40 | ) 41 | -------------------------------------------------------------------------------- /test/geocoders/databc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.exc import GeocoderQueryError 4 | from geopy.geocoders import DataBC 5 | from test.geocoders.util import BaseTestGeocoder 6 | 7 | 8 | class TestDataBC(BaseTestGeocoder): 9 | 10 | @classmethod 11 | def make_geocoder(cls, **kwargs): 12 | return DataBC(**kwargs) 13 | 14 | async def test_user_agent_custom(self): 15 | geocoder = DataBC( 16 | user_agent='my_user_agent/1.0' 17 | ) 18 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 19 | 20 | async def test_geocode(self): 21 | await self.geocode_run( 22 | {"query": "135 North Pym Road, Parksville"}, 23 | {"latitude": 49.321, "longitude": -124.337}, 24 | ) 25 | 26 | async def test_multiple_results(self): 27 | res = await self.geocode_run( 28 | {"query": "1st St", "exactly_one": False}, 29 | {}, 30 | ) 31 | assert len(res) > 1 32 | 33 | async def test_optional_params(self): 34 | await self.geocode_run( 35 | {"query": "5670 malibu terrace nanaimo bc", 36 | "location_descriptor": "accessPoint", 37 | "set_back": 100}, 38 | {"latitude": 49.2299, "longitude": -124.0163}, 39 | ) 40 | 41 | async def test_query_error(self): 42 | with pytest.raises(GeocoderQueryError): 43 | await self.geocode_run( 44 | {"query": "1 Main St, Vancouver", 45 | "location_descriptor": "access_Point"}, 46 | {}, 47 | expect_failure=True, 48 | ) 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version := $(shell python -c 'from geopy import __version__; print(__version__)') 2 | 3 | .PHONY: venv 4 | venv: 5 | [ -d .venv ] || python3 -m venv .venv 6 | # Activate: `. .venv/bin/activate` 7 | 8 | .PHONY: develop 9 | develop: 10 | pip install -e '.[dev]' 11 | 12 | .PHONY: lint lint-flake8 lint-isort 13 | lint-flake8: 14 | flake8 geopy test *.py 15 | lint-isort: 16 | isort --check-only geopy test *.py 17 | lint: lint-flake8 lint-isort 18 | 19 | .PHONY: format 20 | format: 21 | isort geopy test *.py 22 | 23 | .PHONY: test-local 24 | test-local: 25 | @# Run tests without Internet. These are fast and help to avoid 26 | @# spending geocoders quota for test runs which would fail anyway. 27 | python -m pytest --skip-tests-requiring-internet 28 | 29 | .PHONY: test 30 | test: test-local 31 | # Run tests with Internet: 32 | coverage run -m py.test 33 | coverage report 34 | 35 | .PHONY: readme_check 36 | readme_check: 37 | ./setup.py check --restructuredtext --strict 38 | 39 | .PHONY: rst_check 40 | rst_check: 41 | make readme_check 42 | # Doesn't generate any output but prints out errors and warnings. 43 | make -C docs dummy 44 | 45 | .PHONY: clean 46 | clean: 47 | find . -name "*.pyc" -print0 | xargs -0 rm -f 48 | rm -Rf dist 49 | rm -Rf *.egg-info 50 | 51 | .PHONY: docs 52 | docs: 53 | cd docs && make html 54 | 55 | .PHONY: authors 56 | authors: 57 | git log --format='%aN <%aE>' `git describe --abbrev=0 --tags`..@ | sort | uniq >> AUTHORS 58 | cat AUTHORS | sort --ignore-case | uniq >> AUTHORS_ 59 | mv AUTHORS_ AUTHORS 60 | 61 | .PHONY: dist 62 | dist: 63 | make clean 64 | ./setup.py sdist --format=gztar bdist_wheel 65 | 66 | .PHONY: pypi-release 67 | pypi-release: 68 | twine --version 69 | twine upload -s dist/* 70 | 71 | .PHONY: release 72 | release: 73 | make dist 74 | git tag -s $(version) 75 | git push origin $(version) 76 | make pypi-release 77 | 78 | -------------------------------------------------------------------------------- /test/geocoders/tomtom.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import TomTom 2 | from test.geocoders.util import BaseTestGeocoder, env 3 | 4 | 5 | class BaseTestTomTom(BaseTestGeocoder): 6 | 7 | async def test_user_agent_custom(self): 8 | geocoder = self.make_geocoder( 9 | user_agent='my_user_agent/1.0' 10 | ) 11 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 12 | 13 | async def test_geocode(self): 14 | location = await self.geocode_run( 15 | {'query': 'moscow'}, 16 | {'latitude': 55.75587, 'longitude': 37.61768}, 17 | ) 18 | assert 'Moscow' in location.address 19 | 20 | async def test_reverse(self): 21 | location = await self.reverse_run( 22 | {'query': '51.5285057, -0.1369635', 'language': 'en-US'}, 23 | {'latitude': 51.5285057, 'longitude': -0.1369635, 24 | "delta": 0.3}, 25 | ) 26 | assert 'London' in location.address 27 | # Russian Moscow address can be reported differently, so 28 | # we're querying something more ordinary, like London. 29 | # 30 | # For example, AzureMaps might return 31 | # `Красная площадь, 109012 Moskva` instead of the expected 32 | # `Красная площадь, 109012 Москва`, even when language is 33 | # specified explicitly as `ru-RU`. And TomTom always returns 34 | # the cyrillic variant, even when the `en-US` language is 35 | # requested. 36 | 37 | async def test_geocode_empty(self): 38 | await self.geocode_run( 39 | {'query': 'sldkfhdskjfhsdkhgflaskjgf'}, 40 | {}, 41 | expect_failure=True, 42 | ) 43 | 44 | 45 | class TestTomTom(BaseTestTomTom): 46 | 47 | @classmethod 48 | def make_geocoder(cls, **kwargs): 49 | return TomTom(env['TOMTOM_KEY'], timeout=3, 50 | **kwargs) 51 | -------------------------------------------------------------------------------- /geopy/geocoders/geocodeearth.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders.base import DEFAULT_SENTINEL 2 | from geopy.geocoders.pelias import Pelias 3 | 4 | __all__ = ("GeocodeEarth", ) 5 | 6 | 7 | class GeocodeEarth(Pelias): 8 | """geocode.earth, a Pelias-based service provided by the developers 9 | of Pelias itself. 10 | """ 11 | 12 | def __init__( 13 | self, 14 | api_key, 15 | *, 16 | domain='api.geocode.earth', 17 | timeout=DEFAULT_SENTINEL, 18 | proxies=DEFAULT_SENTINEL, 19 | user_agent=None, 20 | scheme=None, 21 | ssl_context=DEFAULT_SENTINEL, 22 | adapter_factory=None 23 | ): 24 | """ 25 | :param str api_key: Geocode.earth API key, required. 26 | 27 | :param str domain: Specify a custom domain for Pelias API. 28 | 29 | :param int timeout: 30 | See :attr:`geopy.geocoders.options.default_timeout`. 31 | 32 | :param dict proxies: 33 | See :attr:`geopy.geocoders.options.default_proxies`. 34 | 35 | :param str user_agent: 36 | See :attr:`geopy.geocoders.options.default_user_agent`. 37 | 38 | :param str scheme: 39 | See :attr:`geopy.geocoders.options.default_scheme`. 40 | 41 | :type ssl_context: :class:`ssl.SSLContext` 42 | :param ssl_context: 43 | See :attr:`geopy.geocoders.options.default_ssl_context`. 44 | 45 | :param callable adapter_factory: 46 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 47 | 48 | .. versionadded:: 2.0 49 | 50 | """ 51 | super().__init__( 52 | api_key=api_key, 53 | domain=domain, 54 | timeout=timeout, 55 | proxies=proxies, 56 | user_agent=user_agent, 57 | scheme=scheme, 58 | ssl_context=ssl_context, 59 | adapter_factory=adapter_factory, 60 | ) 61 | -------------------------------------------------------------------------------- /geopy/__main__.py: -------------------------------------------------------------------------------- 1 | #!/user/bin/env python 2 | from __future__ import print_function 3 | import sys 4 | from math import ceil 5 | import argparse 6 | from . import geocoders 7 | from .exc import GeopyError 8 | from .util import __version__ 9 | 10 | services = sorted(list(geocoders.SERVICE_TO_GEOCODER.keys())) 11 | # formatting the long list of geocoders is a bit tricky 12 | description = ( 13 | 'geopy is a client for several popular geocoding web services.' 14 | '\n\ngeocoders:\n\t' + '\n\t'.join([' '.join(services[i*5:i*5+5]) for i in range(ceil(len(services)/5))]) 15 | ) 16 | epilog = 'Copyright (c) 2006-2016 geopy authors' 17 | usage = 'python -m geopy GEOCODER [address] [address ...]' 18 | 19 | 20 | def main(): 21 | parser = argparse.ArgumentParser(description=description, epilog=epilog, usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter) 22 | parser.add_argument('--version', action='version', version='geopy ' + __version__) 23 | parser.add_argument('geocoder', metavar='geocoder', choices=services, help='One of the above geocoders') 24 | parser.add_argument('addresses', nargs='+', help='One or more addresses to geocode. Addresses must be surrounded by quotes') 25 | parser.add_argument('-c', '--credentials', metavar='key=value', action='append', help=( 26 | 'Arguments to pass when the geocoder is instantiated.' 27 | ' e.g. api_key=. ' 28 | 'May be repeated' 29 | )) 30 | 31 | args = parser.parse_args() 32 | 33 | # Convert key=value to {key: value} 34 | kwargs = {} 35 | if args.credentials: 36 | kwargs = dict(x.split('=') for x in args.credentials) 37 | 38 | geocoder = geocoders.get_geocoder_for_service(args.geocoder)(**kwargs) 39 | 40 | try: 41 | for address in args.addresses: 42 | location = geocoder.geocode(address) 43 | print('{}\t{}'.format(location.latitude, location.longitude)) 44 | 45 | except GeopyError as e: 46 | print(e, file=sys.stderr) 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /test/geocoders/yandex.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.exc import GeocoderInsufficientPrivileges 4 | from geopy.geocoders import Yandex 5 | from test.geocoders.util import BaseTestGeocoder, env 6 | 7 | 8 | class TestYandex(BaseTestGeocoder): 9 | 10 | @classmethod 11 | def make_geocoder(cls, **kwargs): 12 | return Yandex(api_key=env['YANDEX_KEY'], **kwargs) 13 | 14 | async def test_user_agent_custom(self): 15 | geocoder = Yandex( 16 | api_key='mock', 17 | user_agent='my_user_agent/1.0' 18 | ) 19 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 20 | 21 | async def test_geocode(self): 22 | await self.geocode_run( 23 | {"query": "площадь Ленина Донецк"}, 24 | {"latitude": 48.002104, "longitude": 37.805186}, 25 | ) 26 | 27 | async def test_failure_with_invalid_api_key(self): 28 | async with self.inject_geocoder(Yandex(api_key='bad key')): 29 | with pytest.raises(GeocoderInsufficientPrivileges): 30 | await self.geocode_run( 31 | {"query": "площадь Ленина Донецк"}, 32 | {} 33 | ) 34 | 35 | async def test_reverse(self): 36 | await self.reverse_run( 37 | {"query": "40.75376406311989, -73.98489005863667"}, 38 | {"latitude": 40.75376406311989, "longitude": -73.98489005863667}, 39 | ) 40 | 41 | async def test_geocode_lang(self): 42 | await self.geocode_run( 43 | {"query": "площа Леніна Донецьк", "lang": "uk_UA"}, 44 | {"address": "площа Леніна, Донецьк, Україна", 45 | "latitude": 48.002104, "longitude": 37.805186}, 46 | ) 47 | 48 | async def test_reverse_kind(self): 49 | await self.reverse_run( 50 | {"query": (55.743659, 37.408055), "kind": "locality"}, 51 | {"address": "Москва, Россия"} 52 | ) 53 | 54 | async def test_reverse_lang(self): 55 | await self.reverse_run( 56 | {"query": (55.743659, 37.408055), "kind": "locality", 57 | "lang": "uk_UA"}, 58 | {"address": "Москва, Росія"} 59 | ) 60 | -------------------------------------------------------------------------------- /test/geocoders/mapquest.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import MapQuest 2 | from geopy.point import Point 3 | from test.geocoders.util import BaseTestGeocoder, env 4 | 5 | 6 | class TestMapQuest(BaseTestGeocoder): 7 | @classmethod 8 | def make_geocoder(cls, **kwargs): 9 | return MapQuest(api_key=env['MAPQUEST_KEY'], timeout=3, **kwargs) 10 | 11 | async def test_geocode(self): 12 | await self.geocode_run( 13 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 14 | {"latitude": 41.89036, "longitude": -87.624043}, 15 | ) 16 | 17 | async def test_reverse(self): 18 | new_york_point = Point(40.75376406311989, -73.98489005863667) 19 | location = await self.reverse_run( 20 | {"query": new_york_point}, 21 | {"latitude": 40.7537640, "longitude": -73.98489, "delta": 1}, 22 | ) 23 | assert "New York" in location.address 24 | 25 | async def test_zero_results(self): 26 | await self.geocode_run( 27 | {"query": ''}, 28 | {}, 29 | expect_failure=True, 30 | ) 31 | 32 | async def test_geocode_bbox(self): 33 | await self.geocode_run( 34 | { 35 | "query": "435 north michigan ave, chicago il 60611 usa", 36 | "bounds": [Point(35.227672, -103.271484), 37 | Point(48.603858, -74.399414)] 38 | }, 39 | {"latitude": 41.890, "longitude": -87.624}, 40 | ) 41 | 42 | async def test_geocode_raw(self): 43 | result = await self.geocode_run( 44 | {"query": "New York"}, 45 | {"latitude": 40.713054, "longitude": -74.007228, "delta": 1}, 46 | ) 47 | assert result.raw['adminArea1'] == "US" 48 | 49 | async def test_geocode_limit(self): 50 | list_result = await self.geocode_run( 51 | {"query": "maple street", "exactly_one": False, "limit": 2}, 52 | {}, 53 | ) 54 | assert len(list_result) == 2 55 | 56 | list_result = await self.geocode_run( 57 | {"query": "maple street", "exactly_one": False, "limit": 4}, 58 | {}, 59 | ) 60 | assert len(list_result) == 4 61 | -------------------------------------------------------------------------------- /test/geocoders/pelias.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import Pelias 2 | from geopy.point import Point 3 | from test.geocoders.util import BaseTestGeocoder, env 4 | 5 | 6 | class BaseTestPelias(BaseTestGeocoder): 7 | 8 | delta = 0.04 9 | known_state_de = "Verwaltungsregion Ionische Inseln" 10 | known_state_en = "Ionian Islands Periphery" 11 | 12 | async def test_geocode(self): 13 | await self.geocode_run( 14 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 15 | {"latitude": 41.890, "longitude": -87.624}, 16 | ) 17 | await self.geocode_run( 18 | {"query": "san josé california"}, 19 | {"latitude": 37.33939, "longitude": -121.89496}, 20 | ) 21 | 22 | async def test_reverse(self): 23 | await self.reverse_run( 24 | {"query": Point(40.75376406311989, -73.98489005863667)}, 25 | {"latitude": 40.75376406311989, "longitude": -73.98489005863667} 26 | ) 27 | 28 | async def test_boundary_rect(self): 29 | await self.geocode_run( 30 | {"query": "moscow", # Idaho USA 31 | "boundary_rect": [[50.1, -130.1], [44.1, -100.9]]}, 32 | {"latitude": 46.7323875, "longitude": -117.0001651}, 33 | ) 34 | 35 | async def test_geocode_language_parameter(self): 36 | query = "Graben 7, Wien" 37 | result_geocode = await self.geocode_run( 38 | {"query": query, "language": "de"}, {} 39 | ) 40 | assert result_geocode.raw['properties']['country'] == "Österreich" 41 | 42 | result_geocode = await self.geocode_run( 43 | {"query": query, "language": "en"}, {} 44 | ) 45 | assert result_geocode.raw['properties']['country'] == "Austria" 46 | 47 | async def test_reverse_language_parameter(self): 48 | query = "48.198674, 16.348388" 49 | result_reverse_de = await self.reverse_run( 50 | {"query": query, "language": "de"}, 51 | {}, 52 | ) 53 | assert result_reverse_de.raw['properties']['country'] == "Österreich" 54 | 55 | result_reverse_en = await self.reverse_run( 56 | {"query": query, "language": "en"}, 57 | {}, 58 | ) 59 | assert result_reverse_en.raw['properties']['country'] == "Austria" 60 | 61 | 62 | class TestPelias(BaseTestPelias): 63 | 64 | @classmethod 65 | def make_geocoder(cls, **kwargs): 66 | return Pelias( 67 | env['PELIAS_DOMAIN'], 68 | api_key=env['PELIAS_KEY'], 69 | **kwargs, 70 | ) 71 | -------------------------------------------------------------------------------- /geopy/geocoders/azure.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders.base import DEFAULT_SENTINEL 2 | from geopy.geocoders.tomtom import TomTom 3 | 4 | __all__ = ("AzureMaps", ) 5 | 6 | 7 | class AzureMaps(TomTom): 8 | """AzureMaps geocoder based on TomTom. 9 | 10 | Documentation at: 11 | https://docs.microsoft.com/en-us/azure/azure-maps/index 12 | """ 13 | 14 | geocode_path = '/search/address/json' 15 | reverse_path = '/search/address/reverse/json' 16 | 17 | def __init__( 18 | self, 19 | subscription_key, 20 | *, 21 | scheme=None, 22 | timeout=DEFAULT_SENTINEL, 23 | proxies=DEFAULT_SENTINEL, 24 | user_agent=None, 25 | ssl_context=DEFAULT_SENTINEL, 26 | adapter_factory=None, 27 | domain='atlas.microsoft.com' 28 | ): 29 | """ 30 | :param str subscription_key: Azure Maps subscription key. 31 | 32 | :param str scheme: 33 | See :attr:`geopy.geocoders.options.default_scheme`. 34 | 35 | :param int timeout: 36 | See :attr:`geopy.geocoders.options.default_timeout`. 37 | 38 | :param dict proxies: 39 | See :attr:`geopy.geocoders.options.default_proxies`. 40 | 41 | :param str user_agent: 42 | See :attr:`geopy.geocoders.options.default_user_agent`. 43 | 44 | :type ssl_context: :class:`ssl.SSLContext` 45 | :param ssl_context: 46 | See :attr:`geopy.geocoders.options.default_ssl_context`. 47 | 48 | :param callable adapter_factory: 49 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 50 | 51 | .. versionadded:: 2.0 52 | 53 | :param str domain: Domain where the target Azure Maps service 54 | is hosted. 55 | """ 56 | super().__init__( 57 | api_key=subscription_key, 58 | scheme=scheme, 59 | timeout=timeout, 60 | proxies=proxies, 61 | user_agent=user_agent, 62 | ssl_context=ssl_context, 63 | adapter_factory=adapter_factory, 64 | domain=domain, 65 | ) 66 | 67 | def _geocode_params(self, formatted_query): 68 | return { 69 | 'api-version': '1.0', 70 | 'subscription-key': self.api_key, 71 | 'query': formatted_query, 72 | } 73 | 74 | def _reverse_params(self, position): 75 | return { 76 | 'api-version': '1.0', 77 | 'subscription-key': self.api_key, 78 | 'query': position, 79 | } 80 | -------------------------------------------------------------------------------- /geopy/geocoders/pickpoint.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders.base import DEFAULT_SENTINEL 2 | from geopy.geocoders.nominatim import Nominatim 3 | 4 | __all__ = ("PickPoint",) 5 | 6 | 7 | class PickPoint(Nominatim): 8 | """PickPoint geocoder is a commercial version of Nominatim. 9 | 10 | Documentation at: 11 | https://pickpoint.io/api-reference 12 | """ 13 | 14 | geocode_path = '/v1/forward' 15 | reverse_path = '/v1/reverse' 16 | 17 | def __init__( 18 | self, 19 | api_key, 20 | *, 21 | timeout=DEFAULT_SENTINEL, 22 | proxies=DEFAULT_SENTINEL, 23 | domain='api.pickpoint.io', 24 | scheme=None, 25 | user_agent=None, 26 | ssl_context=DEFAULT_SENTINEL, 27 | adapter_factory=None 28 | ): 29 | """ 30 | 31 | :param str api_key: PickPoint API key obtained at 32 | https://pickpoint.io. 33 | 34 | :param int timeout: 35 | See :attr:`geopy.geocoders.options.default_timeout`. 36 | 37 | :param dict proxies: 38 | See :attr:`geopy.geocoders.options.default_proxies`. 39 | 40 | :param str domain: Domain where the target Nominatim service 41 | is hosted. 42 | 43 | :param str scheme: 44 | See :attr:`geopy.geocoders.options.default_scheme`. 45 | 46 | :param str user_agent: 47 | See :attr:`geopy.geocoders.options.default_user_agent`. 48 | 49 | :type ssl_context: :class:`ssl.SSLContext` 50 | :param ssl_context: 51 | See :attr:`geopy.geocoders.options.default_ssl_context`. 52 | 53 | :param callable adapter_factory: 54 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 55 | 56 | .. versionadded:: 2.0 57 | """ 58 | 59 | super().__init__( 60 | timeout=timeout, 61 | proxies=proxies, 62 | domain=domain, 63 | scheme=scheme, 64 | user_agent=user_agent, 65 | ssl_context=ssl_context, 66 | adapter_factory=adapter_factory, 67 | ) 68 | self.api_key = api_key 69 | 70 | def _construct_url(self, base_api, params): 71 | """ 72 | Construct geocoding request url. Overridden. 73 | 74 | :param str base_api: Geocoding function base address - self.api 75 | or self.reverse_api. 76 | 77 | :param dict params: Geocoding params. 78 | 79 | :return: string URL. 80 | """ 81 | params['key'] = self.api_key 82 | return super()._construct_url(base_api, params) 83 | -------------------------------------------------------------------------------- /test/geocoders/geolake.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import Geolake 2 | from test.geocoders.util import BaseTestGeocoder, env 3 | 4 | 5 | class TestUnitGeolake: 6 | 7 | def test_user_agent_custom(self): 8 | geocoder = Geolake( 9 | api_key='DUMMYKEY1234', 10 | user_agent='my_user_agent/1.0' 11 | ) 12 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 13 | 14 | 15 | class TestGeolake(BaseTestGeocoder): 16 | 17 | @classmethod 18 | def make_geocoder(cls, **kwargs): 19 | return Geolake( 20 | api_key=env['GEOLAKE_KEY'], 21 | timeout=10, 22 | **kwargs 23 | ) 24 | 25 | async def test_geocode(self): 26 | await self.geocode_run( 27 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 28 | {"latitude": 41.890344, "longitude": -87.623234, "address": "Chicago, US"}, 29 | ) 30 | 31 | async def test_geocode_country_codes_str(self): 32 | await self.geocode_run( 33 | {"query": "Toronto", "country_codes": "CA"}, 34 | {"latitude": 43.72, "longitude": -79.47, "address": "Toronto, CA"}, 35 | ) 36 | await self.geocode_run( 37 | {"query": "Toronto", "country_codes": "RU"}, 38 | {}, 39 | expect_failure=True 40 | ) 41 | 42 | async def test_geocode_country_codes_list(self): 43 | await self.geocode_run( 44 | {"query": "Toronto", "country_codes": ["CA", "RU"]}, 45 | {"latitude": 43.72, "longitude": -79.47, "address": "Toronto, CA"}, 46 | ) 47 | await self.geocode_run( 48 | {"query": "Toronto", "country_codes": ["UA", "RU"]}, 49 | {}, 50 | expect_failure=True 51 | ) 52 | 53 | async def test_geocode_structured(self): 54 | query = { 55 | "street": "north michigan ave", 56 | "houseNumber": "435", 57 | "city": "chicago", 58 | "state": "il", 59 | "zipcode": 60611, 60 | "country": "US" 61 | } 62 | await self.geocode_run( 63 | {"query": query}, 64 | {"latitude": 41.890344, "longitude": -87.623234} 65 | ) 66 | 67 | async def test_geocode_empty_result(self): 68 | await self.geocode_run( 69 | {"query": "xqj37"}, 70 | {}, 71 | expect_failure=True 72 | ) 73 | 74 | async def test_geocode_missing_city_in_result(self): 75 | await self.geocode_run( 76 | {"query": "H1W 0B4"}, 77 | {"latitude": 45.544952, "longitude": -73.546694, "address": "CA"} 78 | ) 79 | -------------------------------------------------------------------------------- /test/geocoders/geocodio.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy import exc 4 | from geopy.geocoders import Geocodio 5 | from geopy.point import Point 6 | from test.geocoders.util import BaseTestGeocoder, env 7 | 8 | 9 | class TestGeocodio(BaseTestGeocoder): 10 | 11 | @classmethod 12 | def make_geocoder(cls, **kwargs): 13 | return Geocodio(api_key=env['GEOCODIO_KEY'], **kwargs) 14 | 15 | async def test_user_agent_custom(self): 16 | geocoder = self.make_geocoder(user_agent='my_user_agent/1.0') 17 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 18 | 19 | async def test_error_with_only_street(self): 20 | with pytest.raises(exc.GeocoderQueryError): 21 | await self.geocode_run( 22 | {'query': {'street': '435 north michigan ave'}}, 23 | {}, 24 | ) 25 | 26 | async def test_geocode(self): 27 | await self.geocode_run( 28 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 29 | {"latitude": 41.89037, "longitude": -87.623192}, 30 | ) 31 | 32 | async def test_geocode_from_components(self): 33 | await self.geocode_run( 34 | { 35 | "query": { 36 | "street": "435 north michigan ave", 37 | "city": "chicago", 38 | "state": "IL", 39 | "postal_code": "60611" 40 | }, 41 | }, 42 | {"latitude": 41.89037, "longitude": -87.623192}, 43 | ) 44 | 45 | async def test_geocode_many_results(self): 46 | result = await self.geocode_run( 47 | {"query": "Springfield", "exactly_one": False}, 48 | {} 49 | ) 50 | assert len(result) > 1 51 | 52 | async def test_reverse(self): 53 | location = await self.reverse_run( 54 | {"query": Point(40.75376406311989, -73.98489005863667)}, 55 | {"latitude": 40.75376406311989, "longitude": -73.98489005863667}, 56 | ) 57 | assert "new york" in location.address.lower() 58 | 59 | async def test_geocode_no_result(self): 60 | await self.geocode_run( 61 | {"query": "dksajdkjashdkjashdjasghd"}, 62 | {}, 63 | expect_failure=True, 64 | ) 65 | 66 | async def test_geocode_structured_no_result(self): 67 | await self.geocode_run( 68 | {"query": {"city": "dkasdjksahdksajhd", "street": "sdahaskjdhask"}}, 69 | {}, 70 | expect_failure=True, 71 | ) 72 | 73 | async def test_reverse_no_result(self): 74 | await self.reverse_run( 75 | # North Atlantic Ocean 76 | {"query": (35.173809, -37.485351)}, 77 | {}, 78 | expect_failure=True, 79 | ) 80 | -------------------------------------------------------------------------------- /geopy/timezone.py: -------------------------------------------------------------------------------- 1 | from geopy.exc import GeocoderParseError 2 | 3 | try: 4 | import pytz 5 | pytz_available = True 6 | except ImportError: 7 | pytz_available = False 8 | 9 | 10 | __all__ = ( 11 | "Timezone", 12 | ) 13 | 14 | 15 | def ensure_pytz_is_installed(): 16 | if not pytz_available: 17 | raise ImportError( 18 | 'pytz must be installed in order to locate timezones. ' 19 | 'If geopy has been installed with `pip`, then pytz can be ' 20 | 'installed with `pip install "geopy[timezone]"`.' 21 | ) 22 | 23 | 24 | def from_timezone_name(timezone_name, raw): 25 | ensure_pytz_is_installed() 26 | try: 27 | pytz_timezone = pytz.timezone(timezone_name) 28 | except pytz.UnknownTimeZoneError: 29 | raise GeocoderParseError( 30 | "pytz could not parse the timezone identifier (%s) " 31 | "returned by the service." % timezone_name 32 | ) 33 | except KeyError: 34 | raise GeocoderParseError( 35 | "geopy could not find a timezone in this response: %s" % 36 | raw 37 | ) 38 | return Timezone(pytz_timezone, raw) 39 | 40 | 41 | def from_fixed_gmt_offset(gmt_offset_hours, raw): 42 | ensure_pytz_is_installed() 43 | pytz_timezone = pytz.FixedOffset(gmt_offset_hours * 60) 44 | return Timezone(pytz_timezone, raw) 45 | 46 | 47 | class Timezone: 48 | """ 49 | Contains a parsed response for a timezone request, which is 50 | implemented in few geocoders which provide such lookups. 51 | """ 52 | 53 | __slots__ = ("_pytz_timezone", "_raw") 54 | 55 | def __init__(self, pytz_timezone, raw): 56 | self._pytz_timezone = pytz_timezone 57 | self._raw = raw 58 | 59 | @property 60 | def pytz_timezone(self): 61 | """ 62 | pytz timezone instance. 63 | 64 | :rtype: :class:`pytz.tzinfo.BaseTzInfo` 65 | """ 66 | return self._pytz_timezone 67 | 68 | @property 69 | def raw(self): 70 | """ 71 | Timezone's raw, unparsed geocoder response. For details on this, 72 | consult the service's documentation. 73 | 74 | :rtype: dict 75 | """ 76 | return self._raw 77 | 78 | def __str__(self): 79 | return str(self._pytz_timezone) 80 | 81 | def __repr__(self): 82 | return "Timezone(%s)" % repr(self.pytz_timezone) 83 | 84 | def __getstate__(self): 85 | return self._pytz_timezone, self._raw 86 | 87 | def __setstate__(self, state): 88 | self._pytz_timezone, self._raw = state 89 | 90 | def __eq__(self, other): 91 | return ( 92 | isinstance(other, Timezone) and 93 | self._pytz_timezone == other._pytz_timezone and 94 | self.raw == other.raw 95 | ) 96 | 97 | def __ne__(self, other): 98 | return not (self == other) 99 | -------------------------------------------------------------------------------- /geopy/geocoders/openmapquest.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders.base import DEFAULT_SENTINEL 2 | from geopy.geocoders.nominatim import Nominatim 3 | 4 | __all__ = ("OpenMapQuest", ) 5 | 6 | 7 | class OpenMapQuest(Nominatim): 8 | """Geocoder using MapQuest Open Platform Web Services. 9 | 10 | Documentation at: 11 | https://developer.mapquest.com/documentation/open/ 12 | 13 | MapQuest provides two Geocoding APIs: 14 | 15 | - :class:`geopy.geocoders.OpenMapQuest` (this class) Nominatim-alike API 16 | which is based on Open data from OpenStreetMap. 17 | - :class:`geopy.geocoders.MapQuest` MapQuest's own API which is based on 18 | Licensed data. 19 | """ 20 | 21 | geocode_path = '/nominatim/v1/search' 22 | reverse_path = '/nominatim/v1/reverse' 23 | 24 | def __init__( 25 | self, 26 | api_key, 27 | *, 28 | timeout=DEFAULT_SENTINEL, 29 | proxies=DEFAULT_SENTINEL, 30 | domain='open.mapquestapi.com', 31 | scheme=None, 32 | user_agent=None, 33 | ssl_context=DEFAULT_SENTINEL, 34 | adapter_factory=None 35 | ): 36 | """ 37 | 38 | :param str api_key: API key provided by MapQuest, required. 39 | 40 | :param int timeout: 41 | See :attr:`geopy.geocoders.options.default_timeout`. 42 | 43 | :param dict proxies: 44 | See :attr:`geopy.geocoders.options.default_proxies`. 45 | 46 | :param str domain: Domain where the target Nominatim service 47 | is hosted. 48 | 49 | :param str scheme: 50 | See :attr:`geopy.geocoders.options.default_scheme`. 51 | 52 | :param str user_agent: 53 | See :attr:`geopy.geocoders.options.default_user_agent`. 54 | 55 | :type ssl_context: :class:`ssl.SSLContext` 56 | :param ssl_context: 57 | See :attr:`geopy.geocoders.options.default_ssl_context`. 58 | 59 | :param callable adapter_factory: 60 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 61 | 62 | .. versionadded:: 2.0 63 | """ 64 | super().__init__( 65 | timeout=timeout, 66 | proxies=proxies, 67 | domain=domain, 68 | scheme=scheme, 69 | user_agent=user_agent, 70 | ssl_context=ssl_context, 71 | adapter_factory=adapter_factory, 72 | ) 73 | self.api_key = api_key 74 | 75 | def _construct_url(self, base_api, params): 76 | """ 77 | Construct geocoding request url. Overridden. 78 | 79 | :param str base_api: Geocoding function base address - self.api 80 | or self.reverse_api. 81 | 82 | :param dict params: Geocoding params. 83 | 84 | :return: string URL. 85 | """ 86 | params['key'] = self.api_key 87 | return super()._construct_url(base_api, params) 88 | -------------------------------------------------------------------------------- /test/geocoders/photon.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import Photon 2 | from geopy.point import Point 3 | from test.geocoders.util import BaseTestGeocoder 4 | 5 | 6 | class TestPhoton(BaseTestGeocoder): 7 | known_country_de = "Frankreich" 8 | known_country_fr = "France" 9 | 10 | @classmethod 11 | def make_geocoder(cls, **kwargs): 12 | return Photon(**kwargs) 13 | 14 | async def test_user_agent_custom(self): 15 | geocoder = Photon( 16 | user_agent='my_user_agent/1.0' 17 | ) 18 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 19 | 20 | async def test_geocode(self): 21 | location = await self.geocode_run( 22 | {"query": "14 rue pelisson villeurbanne"}, 23 | {"latitude": 45.7733963, "longitude": 4.88612369}, 24 | ) 25 | assert "France" in location.address 26 | 27 | async def test_osm_tag(self): 28 | await self.geocode_run( 29 | {"query": "Freedom", "osm_tag": "tourism:artwork"}, 30 | {"latitude": 38.8898061, "longitude": -77.009088, "delta": 2.0}, 31 | ) 32 | 33 | await self.geocode_run( 34 | {"query": "Freedom", "osm_tag": ["!office", "place:hamlet"]}, 35 | {"latitude": 44.3862491, "longitude": -88.290994, "delta": 2.0}, 36 | ) 37 | 38 | async def test_bbox(self): 39 | await self.geocode_run( 40 | {"query": "moscow"}, 41 | {"latitude": 55.7504461, "longitude": 37.6174943}, 42 | ) 43 | await self.geocode_run( 44 | {"query": "moscow", # Idaho USA 45 | "bbox": [[50.1, -130.1], [44.1, -100.9]]}, 46 | {"latitude": 46.7323875, "longitude": -117.0001651}, 47 | ) 48 | 49 | async def test_reverse(self): 50 | result = await self.reverse_run( 51 | {"query": Point(45.7733105, 4.8869339)}, 52 | {"latitude": 45.7733105, "longitude": 4.8869339} 53 | ) 54 | assert "France" in result.address 55 | 56 | async def test_geocode_language_parameter(self): 57 | result_geocode = await self.geocode_run( 58 | {"query": self.known_country_fr, "language": "de"}, 59 | {}, 60 | ) 61 | assert ( 62 | result_geocode.raw['properties']['country'] == 63 | self.known_country_de 64 | ) 65 | 66 | async def test_reverse_language_parameter(self): 67 | 68 | result_reverse_it = await self.reverse_run( 69 | {"query": "45.7733105, 4.8869339", 70 | "language": "de"}, 71 | {}, 72 | ) 73 | assert ( 74 | result_reverse_it.raw['properties']['country'] == 75 | self.known_country_de 76 | ) 77 | 78 | result_reverse_fr = await self.reverse_run( 79 | {"query": "45.7733105, 4.8869339", 80 | "language": "fr"}, 81 | {}, 82 | ) 83 | assert ( 84 | result_reverse_fr.raw['properties']['country'] == 85 | self.known_country_fr 86 | ) 87 | -------------------------------------------------------------------------------- /test/geocoders/algolia.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import AlgoliaPlaces 2 | from geopy.point import Point 3 | from test.geocoders.util import BaseTestGeocoder, env 4 | 5 | 6 | class TestAlgoliaPlaces(BaseTestGeocoder): 7 | 8 | @classmethod 9 | def make_geocoder(cls, **kwargs): 10 | return AlgoliaPlaces( 11 | app_id=env.get('ALGOLIA_PLACES_APP_ID'), 12 | api_key=env.get('ALGOLIA_PLACES_API_KEY'), 13 | timeout=3, 14 | **kwargs) 15 | 16 | async def test_user_agent_custom(self): 17 | geocoder = self.make_geocoder( 18 | user_agent='my_user_agent/1.0' 19 | ) 20 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 21 | 22 | async def test_geocode(self): 23 | location = await self.geocode_run( 24 | {'query': 'москва'}, 25 | {'latitude': 55.75587, 'longitude': 37.61768}, 26 | ) 27 | assert 'Москва' in location.address 28 | 29 | async def test_reverse(self): 30 | location = await self.reverse_run( 31 | {'query': '51, -0.13', 'language': 'en'}, 32 | {'latitude': 51, 'longitude': -0.13}, 33 | ) 34 | assert 'A272' in location.address 35 | 36 | async def test_explicit_type(self): 37 | location = await self.geocode_run( 38 | {'query': 'Madrid', 'type': 'city', 'language': 'en'}, 39 | {}, 40 | ) 41 | assert 'Madrid' in location.address 42 | 43 | async def test_limit(self): 44 | limit = 5 45 | locations = await self.geocode_run( 46 | {'query': 'Madrid', 'type': 'city', 47 | 'language': 'en', 'exactly_one': False, 48 | 'limit': limit}, 49 | {}, 50 | ) 51 | assert len(locations) == limit 52 | 53 | async def test_countries(self): 54 | countries = ["ES"] 55 | location = await self.geocode_run( 56 | {'query': 'Madrid', 'language': 'en', 57 | 'countries': countries}, 58 | {}, 59 | ) 60 | assert "Madrid" in location.address 61 | 62 | async def test_countries_no_result(self): 63 | countries = ["UA", "RU"] 64 | await self.geocode_run( 65 | {'query': 'Madrid', 'language': 'en', 66 | 'countries': countries}, 67 | {}, 68 | expect_failure=True 69 | ) 70 | 71 | async def test_geocode_no_result(self): 72 | await self.geocode_run( 73 | {'query': 'sldkfhdskjfhsdkhgflaskjgf'}, 74 | {}, 75 | expect_failure=True, 76 | ) 77 | 78 | async def test_around(self): 79 | await self.geocode_run( 80 | {'query': 'maple street', 'language': 'en', 'around': Point(51.1, -0.1)}, 81 | {'latitude': 51.5299, 'longitude': -0.0628044, "delta": 1}, 82 | ) 83 | await self.geocode_run( 84 | {'query': 'maple street', 'language': 'en', 'around': Point(50.1, 10.1)}, 85 | {'latitude': 50.0517, 'longitude': 10.1966, "delta": 1}, 86 | ) 87 | -------------------------------------------------------------------------------- /test/geocoders/mapbox.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.geocoders import MapBox 4 | from geopy.point import Point 5 | from test.geocoders.util import BaseTestGeocoder, env 6 | 7 | 8 | class TestMapBox(BaseTestGeocoder): 9 | @classmethod 10 | def make_geocoder(cls, **kwargs): 11 | return MapBox(api_key=env['MAPBOX_KEY'], timeout=3, **kwargs) 12 | 13 | async def test_geocode(self): 14 | await self.geocode_run( 15 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 16 | {"latitude": 41.890, "longitude": -87.624}, 17 | ) 18 | 19 | async def test_reverse(self): 20 | new_york_point = Point(40.75376406311989, -73.98489005863667) 21 | location = await self.reverse_run( 22 | {"query": new_york_point}, 23 | {"latitude": 40.7537640, "longitude": -73.98489, "delta": 1}, 24 | ) 25 | assert "New York" in location.address 26 | 27 | async def test_zero_results(self): 28 | await self.geocode_run( 29 | {"query": 'asdfasdfasdf'}, 30 | {}, 31 | expect_failure=True, 32 | ) 33 | 34 | async def test_geocode_outside_bbox(self): 35 | await self.geocode_run( 36 | { 37 | "query": "435 north michigan ave, chicago il 60611 usa", 38 | "bbox": [[34.172684, -118.604794], 39 | [34.236144, -118.500938]] 40 | }, 41 | {}, 42 | expect_failure=True, 43 | ) 44 | 45 | async def test_geocode_bbox(self): 46 | await self.geocode_run( 47 | { 48 | "query": "435 north michigan ave, chicago il 60611 usa", 49 | "bbox": [Point(35.227672, -103.271484), 50 | Point(48.603858, -74.399414)] 51 | }, 52 | {"latitude": 41.890, "longitude": -87.624}, 53 | ) 54 | 55 | async def test_geocode_proximity(self): 56 | await self.geocode_run( 57 | {"query": "200 queen street", "proximity": Point(45.3, -66.1)}, 58 | {"latitude": 45.270208, "longitude": -66.050289, "delta": 0.1}, 59 | ) 60 | 61 | async def test_geocode_country_str(self): 62 | await self.geocode_run( 63 | {"query": "kazan", "country": "TR"}, 64 | {"latitude": 40.2317, "longitude": 32.6839}, 65 | ) 66 | 67 | async def test_geocode_country_list(self): 68 | await self.geocode_run( 69 | {"query": "kazan", "country": ["CN", "TR"]}, 70 | {"latitude": 40.2317, "longitude": 32.6839}, 71 | ) 72 | 73 | async def test_geocode_raw(self): 74 | result = await self.geocode_run({"query": "New York"}, {}) 75 | delta = 1.0 76 | expected = pytest.approx((-73.8784155, 40.6930727), abs=delta) 77 | assert expected == result.raw['center'] 78 | 79 | async def test_geocode_exactly_one_false(self): 80 | list_result = await self.geocode_run( 81 | {"query": "maple street", "exactly_one": False}, 82 | {}, 83 | ) 84 | assert len(list_result) >= 3 85 | -------------------------------------------------------------------------------- /test/geocoders/bing.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import Bing 2 | from geopy.point import Point 3 | from test.geocoders.util import BaseTestGeocoder, env 4 | 5 | 6 | class TestUnitBing: 7 | 8 | def test_user_agent_custom(self): 9 | geocoder = Bing( 10 | api_key='DUMMYKEY1234', 11 | user_agent='my_user_agent/1.0' 12 | ) 13 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 14 | 15 | 16 | class TestBing(BaseTestGeocoder): 17 | 18 | @classmethod 19 | def make_geocoder(cls, **kwargs): 20 | return Bing( 21 | api_key=env['BING_KEY'], 22 | **kwargs 23 | ) 24 | 25 | async def test_geocode(self): 26 | await self.geocode_run( 27 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 28 | {"latitude": 41.890, "longitude": -87.624}, 29 | ) 30 | 31 | async def test_reverse_point(self): 32 | await self.reverse_run( 33 | {"query": Point(40.753898, -73.985071)}, 34 | {"latitude": 40.753, "longitude": -73.984}, 35 | ) 36 | 37 | async def test_reverse_with_culture_de(self): 38 | res = await self.reverse_run( 39 | {"query": Point(40.753898, -73.985071), "culture": "DE"}, 40 | {}, 41 | ) 42 | assert "Vereinigte Staaten von Amerika" in res.address 43 | 44 | async def test_reverse_with_culture_en(self): 45 | res = await self.reverse_run( 46 | {"query": Point(40.753898, -73.985071), "culture": "EN"}, 47 | {}, 48 | ) 49 | assert "United States" in res.address 50 | 51 | async def test_reverse_with_include_country_code(self): 52 | res = await self.reverse_run( 53 | {"query": Point(40.753898, -73.985071), 54 | "include_country_code": True}, 55 | {}, 56 | ) 57 | assert res.raw["address"].get("countryRegionIso2", 'missing') == 'US' 58 | 59 | async def test_user_location(self): 60 | pennsylvania = (40.98327, -74.96064) 61 | colorado = (40.160, -105.10) 62 | 63 | pennsylvania_bias = (40.922351, -75.096562) 64 | colorado_bias = (39.914231, -105.070104) 65 | for expected, bias in ((pennsylvania, pennsylvania_bias), 66 | (colorado, colorado_bias)): 67 | await self.geocode_run( 68 | {"query": "20 Main Street", "user_location": Point(bias)}, 69 | {"latitude": expected[0], "longitude": expected[1], "delta": 3.0}, 70 | ) 71 | 72 | async def test_optional_params(self): 73 | res = await self.geocode_run( 74 | {"query": "Badeniho 1, Prague, Czech Republic", 75 | "culture": 'cs', 76 | "include_neighborhood": True, 77 | "include_country_code": True}, 78 | {}, 79 | ) 80 | address = res.raw['address'] 81 | assert address['neighborhood'] == "Praha 6" 82 | assert address['countryRegionIso2'] == "CZ" 83 | 84 | async def test_structured_query(self): 85 | res = await self.geocode_run( 86 | {"query": {'postalCode': '80020', 'countryRegion': 'United States'}}, 87 | {}, 88 | ) 89 | address = res.raw['address'] 90 | assert address['locality'] == "Broomfield" 91 | -------------------------------------------------------------------------------- /test/test_timezone.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pickle 3 | import unittest 4 | 5 | import pytest 6 | 7 | from geopy.timezone import Timezone, from_fixed_gmt_offset, from_timezone_name 8 | 9 | try: 10 | import pytz 11 | import pytz.tzinfo 12 | pytz_available = True 13 | except ImportError: 14 | pytz_available = False 15 | 16 | 17 | @pytest.mark.skipif("not pytz_available") 18 | class TimezoneTestCase(unittest.TestCase): 19 | 20 | timezone_gmt_offset_hours = 3.0 21 | timezone_name = "Europe/Moscow" # a DST-less timezone 22 | 23 | def test_create_from_timezone_name(self): 24 | raw = dict(foo="bar") 25 | tz = from_timezone_name(self.timezone_name, raw) 26 | 27 | self.assertEqual(tz.raw['foo'], 'bar') 28 | self.assertIsInstance(tz.pytz_timezone, pytz.tzinfo.BaseTzInfo) 29 | self.assertIsInstance(tz.pytz_timezone, datetime.tzinfo) 30 | 31 | def test_create_from_fixed_gmt_offset(self): 32 | raw = dict(foo="bar") 33 | tz = from_fixed_gmt_offset(self.timezone_gmt_offset_hours, raw) 34 | 35 | self.assertEqual(tz.raw['foo'], 'bar') 36 | # pytz.FixedOffset is not an instanse of pytz.tzinfo.BaseTzInfo. 37 | self.assertIsInstance(tz.pytz_timezone, datetime.tzinfo) 38 | 39 | olson_tz = pytz.timezone(self.timezone_name) 40 | dt = datetime.datetime.utcnow() 41 | self.assertEqual(tz.pytz_timezone.utcoffset(dt), olson_tz.utcoffset(dt)) 42 | 43 | def test_create_from_pytz_timezone(self): 44 | pytz_timezone = pytz.timezone(self.timezone_name) 45 | tz = Timezone(pytz_timezone, {}) 46 | self.assertIs(tz.pytz_timezone, pytz_timezone) 47 | 48 | def test_string(self): 49 | raw = dict(foo="bar") 50 | tz = from_timezone_name(self.timezone_name, raw) 51 | self.assertEqual(str(tz), self.timezone_name) 52 | 53 | def test_repr(self): 54 | raw = dict(foo="bar") 55 | pytz_timezone = pytz.timezone(self.timezone_name) 56 | tz = Timezone(pytz_timezone, raw) 57 | self.assertEqual(repr(tz), "Timezone(%s)" % repr(pytz_timezone)) 58 | 59 | def test_eq(self): 60 | tz = pytz.timezone("Europe/Paris") 61 | raw1 = dict(a=1) 62 | raw2 = dict(a=1) 63 | self.assertEqual(Timezone(tz, raw1), Timezone(tz, raw2)) 64 | 65 | def test_ne(self): 66 | tz1 = pytz.timezone("Europe/Paris") 67 | tz2 = pytz.timezone("Europe/Prague") 68 | raw = {} 69 | self.assertNotEqual(Timezone(tz1, raw), Timezone(tz2, raw)) 70 | 71 | def test_picklable(self): 72 | raw = dict(foo="bar") 73 | tz = from_timezone_name(self.timezone_name, raw) 74 | # https://docs.python.org/2/library/pickle.html#data-stream-format 75 | for protocol in (0, 1, 2, -1): 76 | pickled = pickle.dumps(tz, protocol=protocol) 77 | tz_unp = pickle.loads(pickled) 78 | self.assertEqual(tz, tz_unp) 79 | 80 | def test_with_unpicklable_raw(self): 81 | some_class = type('some_class', (object,), {}) 82 | raw_unpicklable = dict(missing=some_class()) 83 | del some_class 84 | tz_unpicklable = from_timezone_name(self.timezone_name, raw_unpicklable) 85 | for protocol in (0, 1, 2, -1): 86 | with self.assertRaises((AttributeError, pickle.PicklingError)): 87 | pickle.dumps(tz_unpicklable, protocol=protocol) 88 | -------------------------------------------------------------------------------- /test/geocoders/baidu.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.exc import GeocoderAuthenticationFailure 4 | from geopy.geocoders import Baidu, BaiduV3 5 | from geopy.point import Point 6 | from test.geocoders.util import BaseTestGeocoder, env 7 | 8 | 9 | class TestUnitBaidu(BaseTestGeocoder): 10 | 11 | @classmethod 12 | def make_geocoder(cls, **kwargs): 13 | return Baidu( 14 | api_key='DUMMYKEY1234', 15 | user_agent='my_user_agent/1.0', 16 | **kwargs 17 | ) 18 | 19 | async def test_user_agent_custom(self): 20 | assert self.geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 21 | 22 | 23 | class BaseTestBaidu(BaseTestGeocoder): 24 | 25 | async def test_basic_address(self): 26 | await self.geocode_run( 27 | {"query": ( 28 | "\u5317\u4eac\u5e02\u6d77\u6dc0\u533a" 29 | "\u4e2d\u5173\u6751\u5927\u885727\u53f7" 30 | )}, 31 | {"latitude": 39.983615544507, "longitude": 116.32295155093}, 32 | ) 33 | 34 | async def test_reverse_point(self): 35 | await self.reverse_run( 36 | {"query": Point(39.983615544507, 116.32295155093)}, 37 | {"latitude": 39.983615544507, "longitude": 116.32295155093}, 38 | ) 39 | await self.reverse_run( 40 | {"query": Point(39.983615544507, 116.32295155093), "exactly_one": False}, 41 | {"latitude": 39.983615544507, "longitude": 116.32295155093}, 42 | ) 43 | 44 | 45 | class TestBaidu(BaseTestBaidu): 46 | 47 | @classmethod 48 | def make_geocoder(cls, **kwargs): 49 | return Baidu( 50 | api_key=env['BAIDU_KEY'], 51 | timeout=3, 52 | **kwargs, 53 | ) 54 | 55 | async def test_invalid_ak(self): 56 | async with self.inject_geocoder(Baidu(api_key='DUMMYKEY1234')): 57 | with pytest.raises(GeocoderAuthenticationFailure) as exc_info: 58 | await self.geocode_run({"query": "baidu"}, None) 59 | assert str(exc_info.value) == 'Invalid AK' 60 | 61 | 62 | class TestBaiduSK(BaseTestBaidu): 63 | 64 | @classmethod 65 | def make_geocoder(cls, **kwargs): 66 | return Baidu( 67 | api_key=env['BAIDU_KEY_REQUIRES_SK'], 68 | security_key=env['BAIDU_SEC_KEY'], 69 | timeout=3, 70 | **kwargs, 71 | ) 72 | 73 | async def test_sn_with_peculiar_chars(self): 74 | await self.geocode_run( 75 | {"query": ( 76 | "\u5317\u4eac\u5e02\u6d77\u6dc0\u533a" 77 | "\u4e2d\u5173\u6751\u5927\u885727\u53f7" 78 | " ' & = , ? %" 79 | )}, 80 | {"latitude": 39.983615544507, "longitude": 116.32295155093}, 81 | ) 82 | 83 | 84 | class TestBaiduV3(TestBaidu): 85 | 86 | @classmethod 87 | def make_geocoder(cls, **kwargs): 88 | return BaiduV3( 89 | api_key=env['BAIDU_V3_KEY'], 90 | timeout=3, 91 | **kwargs, 92 | ) 93 | 94 | 95 | class TestBaiduV3SK(TestBaiduSK): 96 | 97 | @classmethod 98 | def make_geocoder(cls, **kwargs): 99 | return BaiduV3( 100 | api_key=env['BAIDU_V3_KEY_REQUIRES_SK'], 101 | security_key=env['BAIDU_V3_SEC_KEY'], 102 | timeout=3, 103 | **kwargs, 104 | ) 105 | -------------------------------------------------------------------------------- /test/geocoders/maptiler.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.geocoders import MapTiler 4 | from geopy.point import Point 5 | from test.geocoders.util import BaseTestGeocoder, env 6 | 7 | 8 | class TestMapTiler(BaseTestGeocoder): 9 | @classmethod 10 | def make_geocoder(cls, **kwargs): 11 | return MapTiler(api_key=env['MAPTILER_KEY'], timeout=3, **kwargs) 12 | 13 | async def test_geocode(self): 14 | await self.geocode_run( 15 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 16 | {"latitude": 41.890, "longitude": -87.624}, 17 | ) 18 | 19 | async def test_reverse(self): 20 | new_york_point = Point(40.75376406311989, -73.98489005863667) 21 | location = await self.reverse_run( 22 | {"query": new_york_point}, 23 | {"latitude": 40.7537640, "longitude": -73.98489, "delta": 1}, 24 | ) 25 | assert "New York" in location.address 26 | 27 | async def test_zero_results(self): 28 | await self.geocode_run( 29 | {"query": 'asdfasdfasdf'}, 30 | {}, 31 | expect_failure=True, 32 | ) 33 | 34 | async def test_geocode_outside_bbox(self): 35 | await self.geocode_run( 36 | { 37 | "query": "435 north michigan ave, chicago il 60611 usa", 38 | "bbox": [[34.172684, -118.604794], 39 | [34.236144, -118.500938]] 40 | }, 41 | {}, 42 | expect_failure=True, 43 | ) 44 | 45 | async def test_geocode_bbox(self): 46 | await self.geocode_run( 47 | { 48 | "query": "435 north michigan ave, chicago il 60611 usa", 49 | "bbox": [Point(35.227672, -103.271484), 50 | Point(48.603858, -74.399414)] 51 | }, 52 | {"latitude": 41.890, "longitude": -87.624}, 53 | ) 54 | 55 | async def test_geocode_proximity(self): 56 | await self.geocode_run( 57 | {"query": "200 queen street", "proximity": Point(45.3, -66.1)}, 58 | {"latitude": 44.038901, "longitude": -64.73052, "delta": 0.1}, 59 | ) 60 | 61 | async def test_reverse_language(self): 62 | zurich_point = Point(47.3723, 8.5422) 63 | location = await self.reverse_run( 64 | {"query": zurich_point, "language": "ja"}, 65 | {"latitude": 47.3723, "longitude": 8.5422, "delta": 1}, 66 | ) 67 | assert "\u30c1\u30e5\u30fc\u30ea\u30c3\u30d2" in location.address 68 | 69 | async def test_geocode_language(self): 70 | location = await self.geocode_run( 71 | {"query": "Z\u00fcrich", "language": "ja", 72 | "proximity": Point(47.3723, 8.5422)}, 73 | {"latitude": 47.3723, "longitude": 8.5422, "delta": 1}, 74 | ) 75 | assert "\u30c1\u30e5\u30fc\u30ea\u30c3\u30d2" in location.address 76 | 77 | async def test_geocode_raw(self): 78 | result = await self.geocode_run({"query": "New York"}, {}) 79 | delta = 1.0 80 | expected = pytest.approx((-73.8784155, 40.6930727), abs=delta) 81 | assert expected == result.raw['center'] 82 | assert "relation175905" == result.raw['properties']['osm_id'] 83 | 84 | async def test_geocode_exactly_one_false(self): 85 | list_result = await self.geocode_run( 86 | {"query": "maple street", "exactly_one": False}, 87 | {}, 88 | ) 89 | assert len(list_result) >= 3 90 | -------------------------------------------------------------------------------- /geopy/format.py: -------------------------------------------------------------------------------- 1 | from geopy import units 2 | 3 | # Unicode characters for symbols that appear in coordinate strings. 4 | DEGREE = chr(176) 5 | PRIME = chr(8242) 6 | DOUBLE_PRIME = chr(8243) 7 | ASCII_DEGREE = '' 8 | ASCII_PRIME = "'" 9 | ASCII_DOUBLE_PRIME = '"' 10 | LATIN1_DEGREE = chr(176) 11 | HTML_DEGREE = '°' 12 | HTML_PRIME = '′' 13 | HTML_DOUBLE_PRIME = '″' 14 | XML_DECIMAL_DEGREE = '°' 15 | XML_DECIMAL_PRIME = '′' 16 | XML_DECIMAL_DOUBLE_PRIME = '″' 17 | XML_HEX_DEGREE = '&xB0;' 18 | XML_HEX_PRIME = '&x2032;' 19 | XML_HEX_DOUBLE_PRIME = '&x2033;' 20 | ABBR_DEGREE = 'deg' 21 | ABBR_ARCMIN = 'arcmin' 22 | ABBR_ARCSEC = 'arcsec' 23 | 24 | DEGREES_FORMAT = ( 25 | "%(degrees)d%(deg)s %(minutes)d%(arcmin)s %(seconds)g%(arcsec)s" 26 | ) 27 | 28 | UNICODE_SYMBOLS = { 29 | 'deg': DEGREE, 30 | 'arcmin': PRIME, 31 | 'arcsec': DOUBLE_PRIME 32 | } 33 | ASCII_SYMBOLS = { 34 | 'deg': ASCII_DEGREE, 35 | 'arcmin': ASCII_PRIME, 36 | 'arcsec': ASCII_DOUBLE_PRIME 37 | } 38 | LATIN1_SYMBOLS = { 39 | 'deg': LATIN1_DEGREE, 40 | 'arcmin': ASCII_PRIME, 41 | 'arcsec': ASCII_DOUBLE_PRIME 42 | } 43 | HTML_SYMBOLS = { 44 | 'deg': HTML_DEGREE, 45 | 'arcmin': HTML_PRIME, 46 | 'arcsec': HTML_DOUBLE_PRIME 47 | } 48 | XML_SYMBOLS = { 49 | 'deg': XML_DECIMAL_DEGREE, 50 | 'arcmin': XML_DECIMAL_PRIME, 51 | 'arcsec': XML_DECIMAL_DOUBLE_PRIME 52 | } 53 | ABBR_SYMBOLS = { 54 | 'deg': ABBR_DEGREE, 55 | 'arcmin': ABBR_ARCMIN, 56 | 'arcsec': ABBR_ARCSEC 57 | } 58 | 59 | 60 | def format_degrees(degrees, fmt=DEGREES_FORMAT, symbols=None): 61 | """ 62 | TODO docs. 63 | """ 64 | symbols = symbols or ASCII_SYMBOLS 65 | arcminutes = units.arcminutes(degrees=degrees - int(degrees)) 66 | arcseconds = units.arcseconds(arcminutes=arcminutes - int(arcminutes)) 67 | format_dict = dict( 68 | symbols, 69 | degrees=degrees, 70 | minutes=abs(arcminutes), 71 | seconds=abs(arcseconds) 72 | ) 73 | return fmt % format_dict 74 | 75 | 76 | DISTANCE_FORMAT = "%(magnitude)s%(unit)s" 77 | DISTANCE_UNITS = { 78 | 'km': lambda d: d, 79 | 'm': lambda d: units.meters(kilometers=d), 80 | 'mi': lambda d: units.miles(kilometers=d), 81 | 'ft': lambda d: units.feet(kilometers=d), 82 | 'nm': lambda d: units.nautical(kilometers=d), 83 | 'nmi': lambda d: units.nautical(kilometers=d) 84 | } 85 | 86 | 87 | def format_distance(kilometers, fmt=DISTANCE_FORMAT, unit='km'): 88 | """ 89 | TODO docs. 90 | """ 91 | magnitude = DISTANCE_UNITS[unit](kilometers) 92 | return fmt % {'magnitude': magnitude, 'unit': unit} 93 | 94 | 95 | _DIRECTIONS = [ 96 | ('north', 'N'), 97 | ('north by east', 'NbE'), 98 | ('north-northeast', 'NNE'), 99 | ('northeast by north', 'NEbN'), 100 | ('northeast', 'NE'), 101 | ('northeast by east', 'NEbE'), 102 | ('east-northeast', 'ENE'), 103 | ('east by north', 'EbN'), 104 | ('east', 'E'), 105 | ('east by south', 'EbS'), 106 | ('east-southeast', 'ESE'), 107 | ('southeast by east', 'SEbE'), 108 | ('southeast', 'SE'), 109 | ('southeast by south', 'SEbS'), 110 | ] 111 | 112 | DIRECTIONS, DIRECTIONS_ABBR = zip(*_DIRECTIONS) 113 | ANGLE_DIRECTIONS = { 114 | n * 11.25: d 115 | for n, d 116 | in enumerate(DIRECTIONS) 117 | } 118 | ANGLE_DIRECTIONS_ABBR = { 119 | n * 11.25: d 120 | for n, d 121 | in enumerate(DIRECTIONS_ABBR) 122 | } 123 | -------------------------------------------------------------------------------- /geopy/exc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions raised by geopy. 3 | """ 4 | 5 | 6 | class GeopyError(Exception): 7 | """ 8 | Geopy-specific exceptions are all inherited from GeopyError. 9 | """ 10 | 11 | 12 | class ConfigurationError(GeopyError, ValueError): 13 | """ 14 | When instantiating a geocoder, the arguments given were invalid. See 15 | the documentation of each geocoder's ``__init__`` for more details. 16 | """ 17 | 18 | 19 | class GeocoderServiceError(GeopyError): 20 | """ 21 | There was an exception caused when calling the remote geocoding service, 22 | and no more specific exception could be raised by geopy. When calling 23 | geocoders' ``geocode`` or `reverse` methods, this is the most generic 24 | exception that can be raised, and any non-geopy exception will be caught 25 | and turned into this. The exception's message will be that of the 26 | original exception. 27 | """ 28 | 29 | 30 | class GeocoderQueryError(GeocoderServiceError, ValueError): 31 | """ 32 | Either geopy detected input that would cause a request to fail, 33 | or a request was made and the remote geocoding service responded 34 | that the request was bad. 35 | """ 36 | 37 | 38 | class GeocoderQuotaExceeded(GeocoderServiceError): 39 | """ 40 | The remote geocoding service refused to fulfill the request 41 | because the client has used its quota. 42 | """ 43 | 44 | 45 | class GeocoderRateLimited(GeocoderQuotaExceeded, IOError): 46 | """ 47 | The remote geocoding service has rate-limited the request. 48 | Retrying later might help. 49 | 50 | Exception of this type has a ``retry_after`` attribute, 51 | which contains amount of time (in seconds) the service 52 | has asked to wait. Might be ``None`` if there were no such 53 | data in response. 54 | 55 | .. versionadded:: 2.2 56 | """ 57 | 58 | def __init__(self, message, *, retry_after=None): 59 | super().__init__(message) 60 | self.retry_after = retry_after 61 | 62 | 63 | class GeocoderAuthenticationFailure(GeocoderServiceError): 64 | """ 65 | The remote geocoding service rejected the API key or account 66 | credentials this geocoder was instantiated with. 67 | """ 68 | 69 | 70 | class GeocoderInsufficientPrivileges(GeocoderServiceError): 71 | """ 72 | The remote geocoding service refused to fulfill a request using the 73 | account credentials given. 74 | """ 75 | 76 | 77 | class GeocoderTimedOut(GeocoderServiceError, TimeoutError): 78 | """ 79 | The call to the geocoding service was aborted because no response 80 | has been received within the ``timeout`` argument of either 81 | the geocoding class or, if specified, the method call. 82 | Some services are just consistently slow, and a higher timeout 83 | may be needed to use them. 84 | """ 85 | 86 | 87 | class GeocoderUnavailable(GeocoderServiceError, IOError): 88 | """ 89 | Either it was not possible to establish a connection to the remote 90 | geocoding service, or the service responded with a code indicating 91 | it was unavailable. 92 | """ 93 | 94 | 95 | class GeocoderParseError(GeocoderServiceError): 96 | """ 97 | Geopy could not parse the service's response. This is probably due 98 | to a bug in geopy. 99 | """ 100 | 101 | 102 | class GeocoderNotFound(GeopyError, ValueError): 103 | """ 104 | Caller requested the geocoder matching a string, e.g., 105 | ``"google"`` > ``GoogleV3``, but no geocoder could be found. 106 | """ 107 | -------------------------------------------------------------------------------- /test/geocoders/arcgis.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy import exc 4 | from geopy.geocoders import ArcGIS 5 | from geopy.point import Point 6 | from test.geocoders.util import BaseTestGeocoder, env 7 | 8 | 9 | class TestUnitArcGIS: 10 | 11 | def test_user_agent_custom(self): 12 | geocoder = ArcGIS( 13 | user_agent='my_user_agent/1.0' 14 | ) 15 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 16 | 17 | 18 | class TestArcGIS(BaseTestGeocoder): 19 | 20 | @classmethod 21 | def make_geocoder(cls, **kwargs): 22 | return ArcGIS(timeout=3, **kwargs) 23 | 24 | async def test_missing_password_error(self): 25 | with pytest.raises(exc.ConfigurationError): 26 | ArcGIS(username='a') 27 | 28 | async def test_scheme_config_error(self): 29 | with pytest.raises(exc.ConfigurationError): 30 | ArcGIS( 31 | username='a', 32 | password='b', 33 | referer='http://www.example.com', 34 | scheme='http' 35 | ) 36 | 37 | async def test_geocode(self): 38 | await self.geocode_run( 39 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 40 | {"latitude": 41.890, "longitude": -87.624}, 41 | ) 42 | 43 | async def test_empty_response(self): 44 | await self.geocode_run( 45 | {"query": "dksahdksahdjksahdoufydshf"}, 46 | {}, 47 | expect_failure=True 48 | ) 49 | 50 | async def test_geocode_with_out_fields_string(self): 51 | result = await self.geocode_run( 52 | {"query": "Trafalgar Square, London", 53 | "out_fields": "Country"}, 54 | {} 55 | ) 56 | assert result.raw['attributes'] == {'Country': 'GBR'} 57 | 58 | async def test_geocode_with_out_fields_list(self): 59 | result = await self.geocode_run( 60 | {"query": "Trafalgar Square, London", 61 | "out_fields": ["City", "Type"]}, 62 | {} 63 | ) 64 | assert result.raw['attributes'] == { 65 | 'City': 'London', 'Type': 'Tourist Attraction' 66 | } 67 | 68 | async def test_reverse_point(self): 69 | location = await self.reverse_run( 70 | {"query": Point(40.753898, -73.985071)}, 71 | {"latitude": 40.75376406311989, "longitude": -73.98489005863667}, 72 | ) 73 | assert 'New York' in location.address 74 | 75 | async def test_reverse_not_exactly_one(self): 76 | await self.reverse_run( 77 | {"query": Point(40.753898, -73.985071), "exactly_one": False}, 78 | {"latitude": 40.75376406311989, "longitude": -73.98489005863667}, 79 | ) 80 | 81 | async def test_reverse_long_label_address(self): 82 | await self.reverse_run( 83 | {"query": (35.173809, -37.485351)}, 84 | {"address": "Atlantic Ocean"}, 85 | ) 86 | 87 | 88 | class TestArcGISAuthenticated(BaseTestGeocoder): 89 | 90 | @classmethod 91 | def make_geocoder(cls, **kwargs): 92 | return ArcGIS( 93 | username=env['ARCGIS_USERNAME'], 94 | password=env['ARCGIS_PASSWORD'], 95 | referer=env['ARCGIS_REFERER'], 96 | timeout=3, 97 | **kwargs 98 | ) 99 | 100 | async def test_basic_address(self): 101 | await self.geocode_run( 102 | {"query": "Potsdamer Platz, Berlin, Deutschland"}, 103 | {"latitude": 52.5094982, "longitude": 13.3765983}, 104 | ) 105 | -------------------------------------------------------------------------------- /geopy/units.py: -------------------------------------------------------------------------------- 1 | """``geopy.units`` module provides utility functions for performing 2 | angle and distance unit conversions. 3 | 4 | Some shortly named aliases are provided for convenience (e.g. 5 | :func:`.km` is an alias for :func:`.kilometers`). 6 | """ 7 | 8 | import math 9 | 10 | # Angles 11 | 12 | 13 | def degrees(radians=0, arcminutes=0, arcseconds=0): 14 | """ 15 | Convert angle to degrees. 16 | """ 17 | deg = 0. 18 | if radians: 19 | deg = math.degrees(radians) 20 | if arcminutes: 21 | deg += arcminutes / arcmin(degrees=1.) 22 | if arcseconds: 23 | deg += arcseconds / arcsec(degrees=1.) 24 | return deg 25 | 26 | 27 | def radians(degrees=0, arcminutes=0, arcseconds=0): 28 | """ 29 | Convert angle to radians. 30 | """ 31 | if arcminutes: 32 | degrees += arcminutes / arcmin(degrees=1.) 33 | if arcseconds: 34 | degrees += arcseconds / arcsec(degrees=1.) 35 | return math.radians(degrees) 36 | 37 | 38 | def arcminutes(degrees=0, radians=0, arcseconds=0): 39 | """ 40 | Convert angle to arcminutes. 41 | """ 42 | if radians: 43 | degrees += math.degrees(radians) 44 | if arcseconds: 45 | degrees += arcseconds / arcsec(degrees=1.) 46 | return degrees * 60. 47 | 48 | 49 | def arcseconds(degrees=0, radians=0, arcminutes=0): 50 | """ 51 | Convert angle to arcseconds. 52 | """ 53 | if radians: 54 | degrees += math.degrees(radians) 55 | if arcminutes: 56 | degrees += arcminutes / arcmin(degrees=1.) 57 | return degrees * 3600. 58 | 59 | 60 | # Lengths 61 | 62 | def kilometers(meters=0, miles=0, feet=0, nautical=0): 63 | """ 64 | Convert distance to kilometers. 65 | """ 66 | ret = 0. 67 | if meters: 68 | ret += meters / 1000. 69 | if feet: 70 | ret += feet / ft(1.) 71 | if nautical: 72 | ret += nautical / nm(1.) 73 | ret += miles * 1.609344 74 | return ret 75 | 76 | 77 | def meters(kilometers=0, miles=0, feet=0, nautical=0): 78 | """ 79 | Convert distance to meters. 80 | """ 81 | return (kilometers + km(nautical=nautical, miles=miles, feet=feet)) * 1000 82 | 83 | 84 | def miles(kilometers=0, meters=0, feet=0, nautical=0): 85 | """ 86 | Convert distance to miles. 87 | """ 88 | ret = 0. 89 | if nautical: 90 | kilometers += nautical / nm(1.) 91 | if feet: 92 | kilometers += feet / ft(1.) 93 | if meters: 94 | kilometers += meters / 1000. 95 | ret += kilometers / 1.609344 96 | return ret 97 | 98 | 99 | def feet(kilometers=0, meters=0, miles=0, nautical=0): 100 | """ 101 | Convert distance to feet. 102 | """ 103 | ret = 0. 104 | if nautical: 105 | kilometers += nautical / nm(1.) 106 | if meters: 107 | kilometers += meters / 1000. 108 | if kilometers: 109 | miles += mi(kilometers=kilometers) 110 | ret += miles * 5280 111 | return ret 112 | 113 | 114 | def nautical(kilometers=0, meters=0, miles=0, feet=0): 115 | """ 116 | Convert distance to nautical miles. 117 | """ 118 | ret = 0. 119 | if feet: 120 | kilometers += feet / ft(1.) 121 | if miles: 122 | kilometers += km(miles=miles) 123 | if meters: 124 | kilometers += meters / 1000. 125 | ret += kilometers / 1.852 126 | return ret 127 | 128 | 129 | # Compatible names 130 | 131 | rad = radians 132 | arcmin = arcminutes 133 | arcsec = arcseconds 134 | km = kilometers 135 | m = meters 136 | mi = miles 137 | ft = feet 138 | nm = nautical 139 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | geopy 4 | """ 5 | 6 | import sys 7 | 8 | from setuptools import find_packages, setup 9 | 10 | if sys.version_info < (3, 5): 11 | raise RuntimeError( 12 | "geopy 2 supports Python 3.5 and above. " 13 | "Use geopy 1.x if you need Python 2.7 or 3.4 support." 14 | ) 15 | 16 | # This import must be below the above `sys.version_info` check, 17 | # because the code being imported here is not compatible with the older 18 | # versions of Python. 19 | from geopy import __version__ as version # noqa # isort:skip 20 | 21 | INSTALL_REQUIRES = [ 22 | 'geographiclib<2,>=1.49', 23 | ] 24 | 25 | EXTRAS_DEV_TESTFILES_COMMON = [ 26 | "async_generator", 27 | ] 28 | 29 | EXTRAS_DEV_LINT = [ 30 | "flake8>=3.8.0,<3.9.0", 31 | "isort>=5.6.0,<5.7.0", 32 | ] 33 | 34 | EXTRAS_DEV_TEST = [ 35 | "coverage", 36 | "pytest-aiohttp", # for `async def` tests 37 | "pytest>=3.10", 38 | "sphinx", # `docutils` from sphinx is used in tests 39 | ] 40 | 41 | EXTRAS_DEV_DOCS = [ 42 | "readme_renderer", 43 | "sphinx", 44 | "sphinx-issues", 45 | "sphinx_rtd_theme>=0.5.0", 46 | ] 47 | 48 | setup( 49 | name='geopy', 50 | version=version, 51 | description='Python Geocoding Toolbox', 52 | long_description=open('README.rst').read(), 53 | maintainer='Kostya Esmukov', 54 | maintainer_email='kostya@esmukov.ru', 55 | url='https://github.com/geopy/geopy', 56 | download_url=( 57 | 'https://github.com/geopy/geopy/archive/%s.tar.gz' % version 58 | ), 59 | packages=find_packages(exclude=["*test*"]), 60 | install_requires=INSTALL_REQUIRES, 61 | extras_require={ 62 | "dev": sorted(set( 63 | EXTRAS_DEV_TESTFILES_COMMON + 64 | EXTRAS_DEV_LINT + 65 | EXTRAS_DEV_TEST + 66 | EXTRAS_DEV_DOCS 67 | )), 68 | "dev-lint": (EXTRAS_DEV_TESTFILES_COMMON + 69 | EXTRAS_DEV_LINT), 70 | "dev-test": (EXTRAS_DEV_TESTFILES_COMMON + 71 | EXTRAS_DEV_TEST), 72 | "dev-docs": EXTRAS_DEV_DOCS, 73 | "aiohttp": ["aiohttp"], 74 | "requests": [ 75 | "urllib3>=1.24.2", 76 | # ^^^ earlier versions would work, but a custom ssl 77 | # context would silently have system certificates be loaded as 78 | # trusted: https://github.com/urllib3/urllib3/pull/1566 79 | 80 | "requests>=2.16.2", 81 | # ^^^ earlier versions would work, but they use an older 82 | # vendored version of urllib3 (see note above) 83 | ], 84 | "timezone": ["pytz"], 85 | }, 86 | license='MIT', 87 | entry_points={ 88 | 'console_scripts': ['geopy=geopy.__main__:main'] 89 | }, 90 | keywords='geocode geocoding gis geographical maps earth distance', 91 | python_requires=">=3.5", 92 | classifiers=[ 93 | "Development Status :: 5 - Production/Stable", 94 | "Intended Audience :: Developers", 95 | "Intended Audience :: Science/Research", 96 | "License :: OSI Approved :: MIT License", 97 | "Operating System :: OS Independent", 98 | "Programming Language :: Python", 99 | "Topic :: Scientific/Engineering :: GIS", 100 | "Topic :: Software Development :: Libraries :: Python Modules", 101 | "Programming Language :: Python :: 3 :: Only", 102 | "Programming Language :: Python :: 3", 103 | "Programming Language :: Python :: 3.5", 104 | "Programming Language :: Python :: 3.6", 105 | "Programming Language :: Python :: 3.7", 106 | "Programming Language :: Python :: 3.8", 107 | "Programming Language :: Python :: 3.9", 108 | "Programming Language :: Python :: Implementation :: CPython", 109 | "Programming Language :: Python :: Implementation :: PyPy", 110 | ] 111 | ) 112 | -------------------------------------------------------------------------------- /test/geocoders/what3words.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | 5 | import geopy.exc 6 | import geopy.geocoders 7 | from geopy.geocoders import What3Words, What3WordsV3 8 | from geopy.geocoders.what3words import _check_query 9 | from test.geocoders.util import BaseTestGeocoder, env 10 | 11 | 12 | class TestUnitWhat3Words: 13 | dummy_api_key = 'DUMMYKEY1234' 14 | 15 | async def test_user_agent_custom(self): 16 | geocoder = What3Words( 17 | api_key=self.dummy_api_key, 18 | user_agent='my_user_agent/1.0' 19 | ) 20 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 21 | 22 | @patch.object(geopy.geocoders.options, 'default_scheme', 'http') 23 | def test_default_scheme_is_ignored(self): 24 | geocoder = What3Words(api_key=self.dummy_api_key) 25 | assert geocoder.scheme == 'https' 26 | 27 | def test_check_query(self): 28 | result_check_threeword_query = _check_query( 29 | "\u0066\u0061\u0068\u0072\u0070\u0072" 30 | "\u0065\u0069\u0073\u002e\u006c\u00fc" 31 | "\u0067\u006e\u0065\u0072\u002e\u006b" 32 | "\u0075\u0074\u0073\u0063\u0068\u0065" 33 | ) 34 | 35 | assert result_check_threeword_query 36 | 37 | 38 | class BaseTestWhat3Words(BaseTestGeocoder): 39 | async def test_geocode(self): 40 | await self.geocode_run( 41 | {"query": "piped.gains.jangle"}, 42 | {"latitude": 53.037611, "longitude": 11.565012}, 43 | ) 44 | 45 | async def test_reverse(self): 46 | await self.reverse_run( 47 | {"query": "53.037611,11.565012", "lang": 'DE'}, 48 | {"address": 'fortschrittliche.voll.schnitt'}, 49 | ) 50 | 51 | async def test_unicode_query(self): 52 | await self.geocode_run( 53 | { 54 | "query": ( 55 | "\u0070\u0069\u0070\u0065\u0064\u002e\u0067" 56 | "\u0061\u0069\u006e\u0073\u002e\u006a\u0061" 57 | "\u006e\u0067\u006c\u0065" 58 | ) 59 | }, 60 | {"latitude": 53.037611, "longitude": 11.565012}, 61 | ) 62 | 63 | async def test_empty_response(self): 64 | with pytest.raises(geopy.exc.GeocoderQueryError): 65 | await self.geocode_run( 66 | {"query": "definitely.not.existingiswearrrr"}, 67 | {}, 68 | expect_failure=True 69 | ) 70 | 71 | async def test_not_exactly_one(self): 72 | await self.geocode_run( 73 | {"query": "piped.gains.jangle", "exactly_one": False}, 74 | {"latitude": 53.037611, "longitude": 11.565012}, 75 | ) 76 | await self.reverse_run( 77 | {"query": (53.037611, 11.565012), "exactly_one": False}, 78 | {"address": "piped.gains.jangle"}, 79 | ) 80 | 81 | async def test_reverse_language(self): 82 | await self.reverse_run( 83 | {"query": (53.037611, 11.565012), "lang": "en", "exactly_one": False}, 84 | {"address": "piped.gains.jangle"}, 85 | ) 86 | 87 | 88 | class TestWhat3Words(BaseTestWhat3Words): 89 | @classmethod 90 | def make_geocoder(cls, **kwargs): 91 | return What3Words( 92 | env['WHAT3WORDS_KEY'], 93 | timeout=3, 94 | **kwargs 95 | ) 96 | 97 | async def test_geocode_language(self): 98 | await self.geocode_run( 99 | {"query": "piped.gains.jangle", "lang": 'DE'}, 100 | {"address": 'fortschrittliche.voll.schnitt'}, 101 | ) 102 | 103 | 104 | class TestWhat3WordsV3(BaseTestWhat3Words): 105 | @classmethod 106 | def make_geocoder(cls, **kwargs): 107 | return What3WordsV3( 108 | env['WHAT3WORDS_KEY'], 109 | timeout=3, 110 | **kwargs 111 | ) 112 | -------------------------------------------------------------------------------- /geopy/location.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | 3 | from geopy.point import Point 4 | 5 | 6 | def _location_tuple(location): 7 | return location._address, (location._point[0], location._point[1]) 8 | 9 | 10 | class Location: 11 | """ 12 | Contains a parsed geocoder response. Can be iterated over as 13 | ``(location, (latitude, longitude' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and an HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " applehelp to make an Apple Help Book" 30 | @echo " devhelp to make HTML files and a Devhelp project" 31 | @echo " epub to make an epub" 32 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 33 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 34 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 35 | @echo " lualatexpdf to make LaTeX files and run them through lualatex" 36 | @echo " xelatexpdf to make LaTeX files and run them through xelatex" 37 | @echo " text to make text files" 38 | @echo " man to make manual pages" 39 | @echo " texinfo to make Texinfo files" 40 | @echo " info to make Texinfo files and run them through makeinfo" 41 | @echo " gettext to make PO message catalogs" 42 | @echo " changes to make an overview of all changed/added/deprecated items" 43 | @echo " xml to make Docutils-native XML files" 44 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 45 | @echo " linkcheck to check all external links for integrity" 46 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 47 | @echo " coverage to run coverage check of the documentation (if enabled)" 48 | @echo " dummy to check syntax errors of document sources" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: latexpdf 55 | latexpdf: 56 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 57 | @echo "Running LaTeX files through pdflatex..." 58 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 59 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 60 | 61 | .PHONY: latexpdfja 62 | latexpdfja: 63 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 64 | @echo "Running LaTeX files through platex and dvipdfmx..." 65 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 66 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 67 | 68 | .PHONY: lualatexpdf 69 | lualatexpdf: 70 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 71 | @echo "Running LaTeX files through lualatex..." 72 | $(MAKE) PDFLATEX=lualatex -C $(BUILDDIR)/latex all-pdf 73 | @echo "lualatex finished; the PDF files are in $(BUILDDIR)/latex." 74 | 75 | .PHONY: xelatexpdf 76 | xelatexpdf: 77 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 78 | @echo "Running LaTeX files through xelatex..." 79 | $(MAKE) PDFLATEX=xelatex -C $(BUILDDIR)/latex all-pdf 80 | @echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex." 81 | 82 | .PHONY: info 83 | info: 84 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 85 | @echo "Running Texinfo files through makeinfo..." 86 | make -C $(BUILDDIR)/texinfo info 87 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 88 | 89 | .PHONY: gettext 90 | gettext: 91 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 92 | 93 | # Catch-all target: route all unknown targets to Sphinx 94 | .PHONY: Makefile 95 | %: Makefile 96 | $(SPHINXBUILD) -b "$@" $(ALLSPHINXOPTS) "$(BUILDDIR)/$@" 97 | -------------------------------------------------------------------------------- /test/geocoders/opencage.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geopy.exc import ( 4 | GeocoderInsufficientPrivileges, 5 | GeocoderQuotaExceeded, 6 | GeocoderRateLimited, 7 | ) 8 | from geopy.geocoders import OpenCage 9 | from test.geocoders.util import BaseTestGeocoder, env 10 | 11 | 12 | class TestUnitOpenCage: 13 | 14 | def test_user_agent_custom(self): 15 | geocoder = OpenCage( 16 | api_key='DUMMYKEY1234', 17 | user_agent='my_user_agent/1.0' 18 | ) 19 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 20 | 21 | 22 | class TestOpenCage(BaseTestGeocoder): 23 | 24 | testing_tokens = { 25 | # https://opencagedata.com/api#testingkeys 26 | 402: "4372eff77b8343cebfc843eb4da4ddc4", 27 | 403: "2e10e5e828262eb243ec0b54681d699a", 28 | 429: "d6d0f0065f4348a4bdfe4587ba02714b", 29 | } 30 | 31 | @classmethod 32 | def make_geocoder(cls, **kwargs): 33 | return OpenCage( 34 | api_key=env['OPENCAGE_KEY'], 35 | timeout=10, 36 | **kwargs 37 | ) 38 | 39 | async def test_geocode(self): 40 | await self.geocode_run( 41 | {"query": "435 north michigan ave, chicago il 60611 usa"}, 42 | {"latitude": 41.890, "longitude": -87.624}, 43 | ) 44 | 45 | async def test_geocode_empty_result(self): 46 | await self.geocode_run( 47 | {"query": "xqj37"}, 48 | {}, 49 | expect_failure=True 50 | ) 51 | 52 | async def test_bounds(self): 53 | await self.geocode_run( 54 | {"query": "moscow", # Idaho USA 55 | "bounds": [[50.1, -130.1], [44.1, -100.9]]}, 56 | {"latitude": 46.7323875, "longitude": -117.0001651}, 57 | ) 58 | 59 | async def test_country_str(self): 60 | await self.geocode_run( 61 | {"query": "kazan", 62 | "country": 'tr'}, 63 | {"latitude": 40.2317, "longitude": 32.6839}, 64 | ) 65 | 66 | async def test_country_list(self): 67 | await self.geocode_run( 68 | {"query": "kazan", 69 | "country": ['cn', 'tr']}, 70 | {"latitude": 40.2317, "longitude": 32.6839}, 71 | ) 72 | 73 | async def test_geocode_annotations(self): 74 | location = await self.geocode_run( 75 | {"query": "london"}, 76 | {"latitude": 51.5073219, "longitude": -0.1276474}, 77 | ) 78 | assert location.raw['annotations'] 79 | 80 | location = await self.geocode_run( 81 | {"query": "london", "annotations": False}, 82 | {"latitude": 51.5073219, "longitude": -0.1276474}, 83 | ) 84 | assert 'annotations' not in location.raw 85 | 86 | async def test_payment_required_error(self, disable_adapter_retries): 87 | async with self.inject_geocoder(OpenCage(api_key=self.testing_tokens[402])): 88 | with pytest.raises(GeocoderQuotaExceeded) as cm: 89 | await self.geocode_run( 90 | {"query": "london"}, {}, skiptest_on_errors=False 91 | ) 92 | assert cm.type is GeocoderQuotaExceeded 93 | # urllib: HTTP Error 402: Payment Required 94 | # others: Non-successful status code 402 95 | 96 | async def test_api_key_disabled_error(self, disable_adapter_retries): 97 | async with self.inject_geocoder(OpenCage(api_key=self.testing_tokens[403])): 98 | with pytest.raises(GeocoderInsufficientPrivileges) as cm: 99 | await self.geocode_run( 100 | {"query": "london"}, {}, skiptest_on_errors=False 101 | ) 102 | assert cm.type is GeocoderInsufficientPrivileges 103 | # urllib: HTTP Error 403: Forbidden 104 | # others: Non-successful status code 403 105 | 106 | async def test_rate_limited_error(self, disable_adapter_retries): 107 | async with self.inject_geocoder(OpenCage(api_key=self.testing_tokens[429])): 108 | with pytest.raises(GeocoderRateLimited) as cm: 109 | await self.geocode_run( 110 | {"query": "london"}, {}, skiptest_on_errors=False 111 | ) 112 | assert cm.type is GeocoderRateLimited 113 | # urllib: HTTP Error 429: Too Many Requests 114 | # others: Non-successful status code 429 115 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | geopy 2 | ===== 3 | 4 | .. image:: https://img.shields.io/pypi/v/geopy.svg?style=flat-square 5 | :target: https://pypi.python.org/pypi/geopy/ 6 | :alt: Latest Version 7 | 8 | .. image:: https://img.shields.io/github/workflow/status/geopy/geopy/CI?style=flat-square 9 | :target: https://github.com/geopy/geopy/actions 10 | :alt: Build Status 11 | 12 | .. image:: https://img.shields.io/github/license/geopy/geopy.svg?style=flat-square 13 | :target: https://pypi.python.org/pypi/geopy/ 14 | :alt: License 15 | 16 | 17 | geopy is a Python client for several popular geocoding web 18 | services. 19 | 20 | geopy makes it easy for Python developers to locate the coordinates of 21 | addresses, cities, countries, and landmarks across the globe using 22 | third-party geocoders and other data sources. 23 | 24 | geopy includes geocoder classes for the `OpenStreetMap Nominatim`_, 25 | `Google Geocoding API (V3)`_, and many other geocoding services. 26 | The full list is available on the `Geocoders doc section`_. 27 | Geocoder classes are located in `geopy.geocoders`_. 28 | 29 | .. _OpenStreetMap Nominatim: https://nominatim.org 30 | .. _Google Geocoding API (V3): https://developers.google.com/maps/documentation/geocoding/ 31 | .. _Geocoders doc section: https://geopy.readthedocs.io/en/latest/#geocoders 32 | .. _geopy.geocoders: https://github.com/geopy/geopy/tree/master/geopy/geocoders 33 | 34 | geopy is tested against CPython (versions 3.5, 3.6, 3.7, 3.8, 3.9) 35 | and PyPy3. geopy 1.x line also supported CPython 2.7, 3.4 and PyPy2. 36 | 37 | © geopy contributors 2006-2018 (see AUTHORS) under the `MIT 38 | License `__. 39 | 40 | Installation 41 | ------------ 42 | 43 | Install using `pip `__ with: 44 | 45 | :: 46 | 47 | pip install geopy 48 | 49 | Or, `download a wheel or source archive from 50 | PyPI `__. 51 | 52 | Geocoding 53 | --------- 54 | 55 | To geolocate a query to an address and coordinates: 56 | 57 | .. code:: pycon 58 | 59 | >>> from geopy.geocoders import Nominatim 60 | >>> geolocator = Nominatim(user_agent="specify_your_app_name_here") 61 | >>> location = geolocator.geocode("175 5th Avenue NYC") 62 | >>> print(location.address) 63 | Flatiron Building, 175, 5th Avenue, Flatiron, New York, NYC, New York, ... 64 | >>> print((location.latitude, location.longitude)) 65 | (40.7410861, -73.9896297241625) 66 | >>> print(location.raw) 67 | {'place_id': '9167009604', 'type': 'attraction', ...} 68 | 69 | To find the address corresponding to a set of coordinates: 70 | 71 | .. code:: pycon 72 | 73 | >>> from geopy.geocoders import Nominatim 74 | >>> geolocator = Nominatim(user_agent="specify_your_app_name_here") 75 | >>> location = geolocator.reverse("52.509669, 13.376294") 76 | >>> print(location.address) 77 | Potsdamer Platz, Mitte, Berlin, 10117, Deutschland, European Union 78 | >>> print((location.latitude, location.longitude)) 79 | (52.5094982, 13.3765983) 80 | >>> print(location.raw) 81 | {'place_id': '654513', 'osm_type': 'node', ...} 82 | 83 | Measuring Distance 84 | ------------------ 85 | 86 | Geopy can calculate geodesic distance between two points using the 87 | `geodesic distance 88 | `_ or the 89 | `great-circle distance 90 | `_, 91 | with a default of the geodesic distance available as the function 92 | `geopy.distance.distance`. 93 | 94 | Here's an example usage of the geodesic distance, taking pair 95 | of :code:`(lat, lon)` tuples: 96 | 97 | .. code:: pycon 98 | 99 | >>> from geopy.distance import geodesic 100 | >>> newport_ri = (41.49008, -71.312796) 101 | >>> cleveland_oh = (41.499498, -81.695391) 102 | >>> print(geodesic(newport_ri, cleveland_oh).miles) 103 | 538.390445368 104 | 105 | Using great-circle distance, also taking pair of :code:`(lat, lon)` tuples: 106 | 107 | .. code:: pycon 108 | 109 | >>> from geopy.distance import great_circle 110 | >>> newport_ri = (41.49008, -71.312796) 111 | >>> cleveland_oh = (41.499498, -81.695391) 112 | >>> print(great_circle(newport_ri, cleveland_oh).miles) 113 | 536.997990696 114 | 115 | Documentation 116 | ------------- 117 | 118 | More documentation and examples can be found at 119 | `Read the Docs `__. 120 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | push: {} 6 | 7 | jobs: 8 | lint: 9 | name: Linting 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: '3.x' 17 | - name: Install dependencies 18 | run: | 19 | python3 -m pip install --upgrade pip 20 | python3 -m pip install tox tox-gh-actions 21 | - run: tox -e lint 22 | 23 | check-docs: 24 | name: RST (README.rst + docs) syntax check 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python 3 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: '3.x' 32 | - name: Install dependencies 33 | run: | 34 | python3 -m pip install --upgrade pip 35 | python3 -m pip install tox tox-gh-actions 36 | - run: tox -e rst 37 | 38 | test_local: 39 | runs-on: ubuntu-latest 40 | strategy: 41 | fail-fast: false 42 | matrix: # &test-matrix 43 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9, 'pypy3'] 44 | experimental: [false] 45 | include: 46 | - python-version: '3.10-dev' 47 | experimental: true 48 | steps: 49 | - uses: actions/checkout@v2 50 | - name: Set up Python ${{ matrix.python-version }} 51 | uses: actions/setup-python@v2 52 | with: 53 | python-version: ${{ matrix.python-version }} 54 | - name: Install dependencies 55 | run: | 56 | python3 -m pip install --upgrade pip setuptools 57 | python3 -m pip install tox tox-gh-actions 58 | - run: GEOPY_TOX_TARGET=test-local tox 59 | 60 | test_full: 61 | if: ${{ github.event_name != 'pull_request' }} 62 | needs: [lint, check-docs, test_local] 63 | runs-on: ubuntu-latest 64 | strategy: 65 | fail-fast: false 66 | matrix: # *test-matrix https://github.community/t/support-for-yaml-anchors/16128 67 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9, 'pypy3'] 68 | experimental: [false] 69 | include: 70 | - python-version: '3.10-dev' 71 | experimental: true 72 | max-parallel: 2 # Reduce load on the geocoding services 73 | steps: 74 | - uses: actions/checkout@v2 75 | - name: Set up Python ${{ matrix.python-version }} 76 | uses: actions/setup-python@v2 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | - name: Install dependencies 80 | run: | 81 | python3 -m pip install --upgrade pip setuptools 82 | python3 -m pip install tox tox-gh-actions 83 | - run: tox 84 | env: 85 | # GitHub Actions cannot just pass all secrets as env vars :( 86 | # 87 | # Please preserve alphabetical order. 88 | ALGOLIA_PLACES_API_KEY: ${{ secrets.ALGOLIA_PLACES_API_KEY }} 89 | ALGOLIA_PLACES_APP_ID: ${{ secrets.ALGOLIA_PLACES_APP_ID }} 90 | ARCGIS_PASSWORD: ${{ secrets.ARCGIS_PASSWORD }} 91 | ARCGIS_REFERER: ${{ secrets.ARCGIS_REFERER }} 92 | ARCGIS_USERNAME: ${{ secrets.ARCGIS_USERNAME }} 93 | AZURE_SUBSCRIPTION_KEY: ${{ secrets.AZURE_SUBSCRIPTION_KEY }} 94 | BAIDU_KEY: ${{ secrets.BAIDU_KEY }} 95 | BAIDU_KEY_REQUIRES_SK: ${{ secrets.BAIDU_KEY_REQUIRES_SK }} 96 | BAIDU_SEC_KEY: ${{ secrets.BAIDU_SEC_KEY }} 97 | BAIDU_V3_KEY: ${{ secrets.BAIDU_V3_KEY }} 98 | BAIDU_V3_KEY_REQUIRES_SK: ${{ secrets.BAIDU_V3_KEY_REQUIRES_SK }} 99 | BAIDU_V3_SEC_KEY: ${{ secrets.BAIDU_V3_SEC_KEY }} 100 | BING_KEY: ${{ secrets.BING_KEY }} 101 | GEOCODEEARTH_KEY: ${{ secrets.GEOCODEEARTH_KEY }} 102 | GEOCODIO_KEY: ${{ secrets.GEOCODIO_KEY }} 103 | GEOLAKE_KEY: ${{ secrets.GEOLAKE_KEY }} 104 | GEONAMES_USERNAME: ${{ secrets.GEONAMES_USERNAME }} 105 | GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }} 106 | HERE_APIKEY: ${{ secrets.HERE_APIKEY }} 107 | HERE_APP_CODE: ${{ secrets.HERE_APP_CODE }} 108 | HERE_APP_ID: ${{ secrets.HERE_APP_ID }} 109 | IGNFRANCE_KEY: ${{ secrets.IGNFRANCE_KEY }} 110 | IGNFRANCE_REFERER: ${{ secrets.IGNFRANCE_REFERER }} 111 | LIVESTREETS_AUTH_ID: ${{ secrets.LIVESTREETS_AUTH_ID }} 112 | LIVESTREETS_AUTH_TOKEN: ${{ secrets.LIVESTREETS_AUTH_TOKEN }} 113 | MAPBOX_KEY: ${{ secrets.MAPBOX_KEY }} 114 | MAPQUEST_KEY: ${{ secrets.MAPQUEST_KEY }} 115 | MAPTILER_KEY: ${{ secrets.MAPTILER_KEY }} 116 | OPENCAGE_KEY: ${{ secrets.OPENCAGE_KEY }} 117 | OPENMAPQUEST_APIKEY: ${{ secrets.OPENMAPQUEST_APIKEY }} 118 | PICKPOINT_KEY: ${{ secrets.PICKPOINT_KEY }} 119 | TOMTOM_KEY: ${{ secrets.TOMTOM_KEY }} 120 | WHAT3WORDS_KEY: ${{ secrets.WHAT3WORDS_KEY }} 121 | YANDEX_KEY: ${{ secrets.YANDEX_KEY }} 122 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Adam Tygart 2 | Adrián López 3 | Afonso Queiros 4 | Albina 5 | Alessandro Pasotti 6 | Andrea Tosatto 7 | Ann Paul 8 | Antonis Kanouras 9 | Armin Leuprecht 10 | Arsen Mamikonyan 11 | Arsen Mamikonyan 12 | Arthur Pemberton 13 | Artur 14 | avdd 15 | Azimjon Pulatov <35038240+azimjohn@users.noreply.github.com> 16 | Benjamin Henne 17 | Benjamin Trigona-Harany 18 | Benjamin Trigona-Harany 19 | Benoit Grégoire 20 | Bernd Schlapsi 21 | Brian Beck 22 | Charles Karney 23 | chilfing 24 | crccheck 25 | Dale 26 | Daniel Thul 27 | Danny Finkelstein 28 | Dave Arter 29 | David Gilman 30 | David Mueller 31 | deeplook 32 | Demeter Sztanko 33 | Dmitrii K 34 | Dody Suria Wijaya 35 | dutko.adam 36 | Edward Betts 37 | Emile Aben 38 | enrique a <13837490+enriqueav@users.noreply.github.com> 39 | Eric Palakovich Carr 40 | exogen 41 | Fabien Reboia 42 | Feanil Patel 43 | gary.bernhardt 44 | Gregory Nicholas 45 | groovecoder 46 | Hannes 47 | Hanno Schlichting 48 | Holger Bruch 49 | Ian Edwards 50 | Ian Wilson 51 | ijl 52 | ironfroggy 53 | Isaac Sijaranamual 54 | James Maddox 55 | James Mills 56 | jhmaddox 57 | Joel Natividad 58 | John.L.Clark 59 | Jon Duckworth 60 | Jonathan Batchelor 61 | Jordan Bouvier 62 | Jose Martin 63 | jqnatividad 64 | Karimov Dmitriy 65 | Kostya Esmukov 66 | Luca Marra 67 | Luke Hubbard 68 | Magnus Hiie 69 | Marc-Olivier Titeux 70 | Marco Milanesi 71 | Mariana Georgieva 72 | Martin 73 | Mateusz Konieczny 74 | Mesut Öncel 75 | Micah Cochran 76 | michal 77 | Michal Migurski 78 | Mike Hansen 79 | Mike Taves 80 | Mike Tigas 81 | Mike Toews 82 | Miltos 83 | mtmail 84 | mz 85 | navidata 86 | nucflash 87 | Oleg 88 | Oskar Hollmann 89 | Pavel 90 | Paweł Mandera 91 | Pedro Rodrigues 92 | Peter Gullekson 93 | Philip Kimmey 94 | Pratheek Rebala 95 | Przemek Malolepszy <39582596+szogoon@users.noreply.github.com> 96 | Risent Zhang 97 | Rocky Meza 98 | Ryan Nagle 99 | Sarah Hoffmann 100 | Saïd Tezel 101 | scottessner 102 | Sebastian Illing 103 | Sebastian Neubauer 104 | SemiNormal 105 | Sergey Lyapustin 106 | Sergio Martín Morillas 107 | Serphentas 108 | svalee 109 | Svetlana Konovalova 110 | Sébastien Barré 111 | TheRealZeljko 112 | Thomas 113 | Tim Gates 114 | Tom Wallroth 115 | tony 116 | tristan 117 | Vladimir Kalinkin 118 | William Hammond 119 | willr 120 | Yorick Holkamp 121 | yrafalin <31785347+yrafalin@users.noreply.github.com> 122 | zhongjun-ma <58385923+zhongjun-ma@users.noreply.github.com> 123 | Álvaro Mondéjar 124 | -------------------------------------------------------------------------------- /geopy/geocoders/smartystreets.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import urlencode 3 | 4 | from geopy.adapters import AdapterHTTPError 5 | from geopy.exc import GeocoderQuotaExceeded 6 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 7 | from geopy.location import Location 8 | from geopy.util import logger 9 | 10 | __all__ = ("LiveAddress", ) 11 | 12 | 13 | class LiveAddress(Geocoder): 14 | """Geocoder using the LiveAddress API provided by SmartyStreets. 15 | 16 | Documentation at: 17 | https://smartystreets.com/docs/cloud/us-street-api 18 | """ 19 | 20 | geocode_path = '/street-address' 21 | 22 | def __init__( 23 | self, 24 | auth_id, 25 | auth_token, 26 | *, 27 | timeout=DEFAULT_SENTINEL, 28 | proxies=DEFAULT_SENTINEL, 29 | user_agent=None, 30 | ssl_context=DEFAULT_SENTINEL, 31 | adapter_factory=None 32 | ): 33 | """ 34 | 35 | :param str auth_id: Valid `Auth ID` from SmartyStreets. 36 | 37 | :param str auth_token: Valid `Auth Token` from SmartyStreets. 38 | 39 | :param int timeout: 40 | See :attr:`geopy.geocoders.options.default_timeout`. 41 | 42 | :param dict proxies: 43 | See :attr:`geopy.geocoders.options.default_proxies`. 44 | 45 | :param str user_agent: 46 | See :attr:`geopy.geocoders.options.default_user_agent`. 47 | 48 | :type ssl_context: :class:`ssl.SSLContext` 49 | :param ssl_context: 50 | See :attr:`geopy.geocoders.options.default_ssl_context`. 51 | 52 | :param callable adapter_factory: 53 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 54 | 55 | .. versionadded:: 2.0 56 | """ 57 | super().__init__( 58 | scheme='https', 59 | timeout=timeout, 60 | proxies=proxies, 61 | user_agent=user_agent, 62 | ssl_context=ssl_context, 63 | adapter_factory=adapter_factory, 64 | ) 65 | self.auth_id = auth_id 66 | self.auth_token = auth_token 67 | 68 | domain = 'api.smartystreets.com' 69 | self.api = '%s://%s%s' % (self.scheme, domain, self.geocode_path) 70 | 71 | def geocode( 72 | self, 73 | query, 74 | *, 75 | exactly_one=True, 76 | timeout=DEFAULT_SENTINEL, 77 | candidates=1 78 | ): 79 | """ 80 | Return a location point by address. 81 | 82 | :param str query: The address or query you wish to geocode. 83 | 84 | :param bool exactly_one: Return one result or a list of results, if 85 | available. 86 | 87 | :param int timeout: Time, in seconds, to wait for the geocoding service 88 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 89 | exception. Set this only if you wish to override, on this call 90 | only, the value set during the geocoder's initialization. 91 | 92 | :param int candidates: An integer between 1 and 10 indicating the max 93 | number of candidate addresses to return if a valid address 94 | could be found. 95 | 96 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 97 | ``exactly_one=False``. 98 | """ 99 | 100 | if not (1 <= candidates <= 10): 101 | raise ValueError('candidates must be between 1 and 10') 102 | 103 | query = { 104 | 'auth-id': self.auth_id, 105 | 'auth-token': self.auth_token, 106 | 'street': query, 107 | 'candidates': candidates, 108 | } 109 | url = '{url}?{query}'.format(url=self.api, query=urlencode(query)) 110 | 111 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 112 | callback = partial(self._parse_json, exactly_one=exactly_one) 113 | return self._call_geocoder(url, callback, timeout=timeout) 114 | 115 | def _geocoder_exception_handler(self, error): 116 | search = "no active subscriptions found" 117 | if isinstance(error, AdapterHTTPError): 118 | if search in str(error).lower(): 119 | raise GeocoderQuotaExceeded(str(error)) from error 120 | if search in (error.text or "").lower(): 121 | raise GeocoderQuotaExceeded(error.text) from error 122 | 123 | def _parse_json(self, response, exactly_one=True): 124 | """ 125 | Parse responses as JSON objects. 126 | """ 127 | if not len(response): 128 | return None 129 | if exactly_one: 130 | return self._format_structured_address(response[0]) 131 | else: 132 | return [self._format_structured_address(c) for c in response] 133 | 134 | def _format_structured_address(self, address): 135 | """ 136 | Pretty-print address and return lat, lon tuple. 137 | """ 138 | latitude = address['metadata'].get('latitude') 139 | longitude = address['metadata'].get('longitude') 140 | return Location( 141 | ", ".join((address['delivery_line_1'], address['last_line'])), 142 | (latitude, longitude) if latitude and longitude else None, 143 | address 144 | ) 145 | -------------------------------------------------------------------------------- /test/test_location.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import unittest 3 | 4 | from geopy.location import Location 5 | from geopy.point import Point 6 | 7 | GRAND_CENTRAL_STR = "89 E 42nd St New York, NY 10017" 8 | 9 | GRAND_CENTRAL_COORDS_STR = "40.752662,-73.9773" 10 | GRAND_CENTRAL_COORDS_TUPLE = (40.752662, -73.9773, 0) 11 | GRAND_CENTRAL_POINT = Point(GRAND_CENTRAL_COORDS_STR) 12 | 13 | GRAND_CENTRAL_RAW = { 14 | 'id': '1', 15 | 'class': 'place', 16 | 'lat': '40.752662', 17 | 'lon': '-73.9773', 18 | 'display_name': 19 | "89, East 42nd Street, New York, " 20 | "New York, 10017, United States of America", 21 | } 22 | 23 | 24 | class LocationTestCase(unittest.TestCase): 25 | 26 | def _location_iter_test( 27 | self, 28 | loc, 29 | ref_address=GRAND_CENTRAL_STR, 30 | ref_longitude=GRAND_CENTRAL_COORDS_TUPLE[0], 31 | ref_latitude=GRAND_CENTRAL_COORDS_TUPLE[1] 32 | ): 33 | address, (latitude, longitude) = loc 34 | self.assertEqual(address, ref_address) 35 | self.assertEqual(latitude, ref_longitude) 36 | self.assertEqual(longitude, ref_latitude) 37 | 38 | def _location_properties_test(self, loc, raw=None): 39 | self.assertEqual(loc.address, GRAND_CENTRAL_STR) 40 | self.assertEqual(loc.latitude, GRAND_CENTRAL_COORDS_TUPLE[0]) 41 | self.assertEqual(loc.longitude, GRAND_CENTRAL_COORDS_TUPLE[1]) 42 | self.assertEqual(loc.altitude, GRAND_CENTRAL_COORDS_TUPLE[2]) 43 | if raw is not None: 44 | self.assertEqual(loc.raw, raw) 45 | 46 | def test_location_str(self): 47 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_COORDS_STR, {}) 48 | self._location_iter_test(loc) 49 | self.assertEqual(loc.point, GRAND_CENTRAL_POINT) 50 | 51 | def test_location_point(self): 52 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 53 | self._location_iter_test(loc) 54 | self.assertEqual(loc.point, GRAND_CENTRAL_POINT) 55 | 56 | def test_location_none(self): 57 | with self.assertRaises(TypeError): 58 | Location(GRAND_CENTRAL_STR, None, {}) 59 | 60 | def test_location_iter(self): 61 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_COORDS_TUPLE, {}) 62 | self._location_iter_test(loc) 63 | self.assertEqual(loc.point, GRAND_CENTRAL_POINT) 64 | 65 | def test_location_point_typeerror(self): 66 | with self.assertRaises(TypeError): 67 | Location(GRAND_CENTRAL_STR, 1, {}) 68 | 69 | def test_location_array_access(self): 70 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_COORDS_TUPLE, {}) 71 | self.assertEqual(loc[0], GRAND_CENTRAL_STR) 72 | self.assertEqual(loc[1][0], GRAND_CENTRAL_COORDS_TUPLE[0]) 73 | self.assertEqual(loc[1][1], GRAND_CENTRAL_COORDS_TUPLE[1]) 74 | 75 | def test_location_properties(self): 76 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 77 | self._location_properties_test(loc) 78 | 79 | def test_location_raw(self): 80 | loc = Location( 81 | GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, raw=GRAND_CENTRAL_RAW 82 | ) 83 | self._location_properties_test(loc, GRAND_CENTRAL_RAW) 84 | 85 | def test_location_string(self): 86 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 87 | self.assertEqual(str(loc), loc.address) 88 | 89 | def test_location_len(self): 90 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 91 | self.assertEqual(len(loc), 2) 92 | 93 | def test_location_eq(self): 94 | loc1 = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 95 | loc2 = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_COORDS_TUPLE, {}) 96 | self.assertEqual(loc1, loc2) 97 | 98 | def test_location_ne(self): 99 | loc1 = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 100 | loc2 = Location(GRAND_CENTRAL_STR, Point(0, 0), {}) 101 | self.assertNotEqual(loc1, loc2) 102 | 103 | def test_location_repr(self): 104 | address = ( 105 | "22, Ksi\u0119dza Paw\u0142a Po\u015bpiecha, " 106 | "Centrum Po\u0142udnie, Zabrze, wojew\xf3dztwo " 107 | "\u015bl\u0105skie, 41-800, Polska" 108 | ) 109 | point = (0.0, 0.0, 0.0) 110 | loc = Location(address, point, {}) 111 | self.assertEqual( 112 | repr(loc), 113 | "Location(%s, %r)" % (address, point) 114 | ) 115 | 116 | def test_location_is_picklable(self): 117 | loc = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, {}) 118 | # https://docs.python.org/2/library/pickle.html#data-stream-format 119 | for protocol in (0, 1, 2, -1): 120 | pickled = pickle.dumps(loc, protocol=protocol) 121 | loc_unp = pickle.loads(pickled) 122 | self.assertEqual(loc, loc_unp) 123 | 124 | def test_location_with_unpicklable_raw(self): 125 | some_class = type('some_class', (object,), {}) 126 | raw_unpicklable = dict(missing=some_class()) 127 | del some_class 128 | loc_unpicklable = Location(GRAND_CENTRAL_STR, GRAND_CENTRAL_POINT, 129 | raw_unpicklable) 130 | for protocol in (0, 1, 2, -1): 131 | with self.assertRaises((AttributeError, pickle.PicklingError)): 132 | pickle.dumps(loc_unpicklable, protocol=protocol) 133 | -------------------------------------------------------------------------------- /geopy/geocoders/databc.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import urlencode 3 | 4 | from geopy.exc import GeocoderQueryError 5 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 6 | from geopy.location import Location 7 | from geopy.util import logger 8 | 9 | __all__ = ("DataBC", ) 10 | 11 | 12 | class DataBC(Geocoder): 13 | """Geocoder using the Physical Address Geocoder from DataBC. 14 | 15 | Documentation at: 16 | http://www.data.gov.bc.ca/dbc/geographic/locate/geocoding.page 17 | """ 18 | 19 | geocode_path = '/pub/geocoder/addresses.geojson' 20 | 21 | def __init__( 22 | self, 23 | *, 24 | scheme=None, 25 | timeout=DEFAULT_SENTINEL, 26 | proxies=DEFAULT_SENTINEL, 27 | user_agent=None, 28 | ssl_context=DEFAULT_SENTINEL, 29 | adapter_factory=None 30 | ): 31 | """ 32 | 33 | :param str scheme: 34 | See :attr:`geopy.geocoders.options.default_scheme`. 35 | 36 | :param int timeout: 37 | See :attr:`geopy.geocoders.options.default_timeout`. 38 | 39 | :param dict proxies: 40 | See :attr:`geopy.geocoders.options.default_proxies`. 41 | 42 | :param str user_agent: 43 | See :attr:`geopy.geocoders.options.default_user_agent`. 44 | 45 | :type ssl_context: :class:`ssl.SSLContext` 46 | :param ssl_context: 47 | See :attr:`geopy.geocoders.options.default_ssl_context`. 48 | 49 | :param callable adapter_factory: 50 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 51 | 52 | .. versionadded:: 2.0 53 | """ 54 | super().__init__( 55 | scheme=scheme, 56 | timeout=timeout, 57 | proxies=proxies, 58 | user_agent=user_agent, 59 | ssl_context=ssl_context, 60 | adapter_factory=adapter_factory, 61 | ) 62 | domain = 'apps.gov.bc.ca' 63 | self.api = '%s://%s%s' % (self.scheme, domain, self.geocode_path) 64 | 65 | def geocode( 66 | self, 67 | query, 68 | *, 69 | max_results=25, 70 | set_back=0, 71 | location_descriptor='any', 72 | exactly_one=True, 73 | timeout=DEFAULT_SENTINEL 74 | ): 75 | """ 76 | Return a location point by address. 77 | 78 | :param str query: The address or query you wish to geocode. 79 | 80 | :param int max_results: The maximum number of resutls to request. 81 | 82 | :param float set_back: The distance to move the accessPoint away 83 | from the curb (in meters) and towards the interior of the parcel. 84 | location_descriptor must be set to accessPoint for set_back to 85 | take effect. 86 | 87 | :param str location_descriptor: The type of point requested. It 88 | can be any, accessPoint, frontDoorPoint, parcelPoint, 89 | rooftopPoint and routingPoint. 90 | 91 | :param bool exactly_one: Return one result or a list of results, if 92 | available. 93 | 94 | :param int timeout: Time, in seconds, to wait for the geocoding service 95 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 96 | exception. Set this only if you wish to override, on this call 97 | only, the value set during the geocoder's initialization. 98 | 99 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 100 | ``exactly_one=False``. 101 | """ 102 | params = {'addressString': query} 103 | if set_back != 0: 104 | params['setBack'] = set_back 105 | if location_descriptor not in ['any', 106 | 'accessPoint', 107 | 'frontDoorPoint', 108 | 'parcelPoint', 109 | 'rooftopPoint', 110 | 'routingPoint']: 111 | raise GeocoderQueryError( 112 | "You did not provided a location_descriptor " 113 | "the webservice can consume. It should be any, accessPoint, " 114 | "frontDoorPoint, parcelPoint, rooftopPoint or routingPoint." 115 | ) 116 | params['locationDescriptor'] = location_descriptor 117 | if exactly_one: 118 | max_results = 1 119 | params['maxResults'] = max_results 120 | 121 | url = "?".join((self.api, urlencode(params))) 122 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 123 | callback = partial(self._parse_json, exactly_one=exactly_one) 124 | return self._call_geocoder(url, callback, timeout=timeout) 125 | 126 | def _parse_json(self, response, exactly_one): 127 | # Success; convert from GeoJSON 128 | if not len(response['features']): 129 | return None 130 | geocoded = [] 131 | for feature in response['features']: 132 | geocoded.append(self._parse_feature(feature)) 133 | if exactly_one: 134 | return geocoded[0] 135 | return geocoded 136 | 137 | def _parse_feature(self, feature): 138 | properties = feature['properties'] 139 | coordinates = feature['geometry']['coordinates'] 140 | return Location( 141 | properties['fullAddress'], (coordinates[1], coordinates[0]), 142 | properties 143 | ) 144 | -------------------------------------------------------------------------------- /test/extra/rate_limiter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import MagicMock, patch, sentinel 3 | 4 | import pytest 5 | 6 | from geopy.exc import GeocoderQuotaExceeded, GeocoderServiceError 7 | from geopy.extra.rate_limiter import AsyncRateLimiter, RateLimiter 8 | 9 | 10 | @pytest.fixture(params=[False, True]) 11 | def is_async(request): 12 | return request.param 13 | 14 | 15 | @pytest.fixture 16 | def auto_async(is_async): 17 | if is_async: 18 | async def auto_async(coro): 19 | return await coro 20 | else: 21 | async def auto_async(result): 22 | return result 23 | return auto_async 24 | 25 | 26 | @pytest.fixture 27 | def auto_async_side_effect(is_async): 28 | def auto_async_side_effect(side_effect): 29 | if not is_async: 30 | return side_effect 31 | 32 | mock = MagicMock(side_effect=side_effect) 33 | 34 | async def func(*args, **kwargs): 35 | return mock(*args, **kwargs) 36 | 37 | return func 38 | 39 | return auto_async_side_effect 40 | 41 | 42 | @pytest.fixture 43 | def rate_limiter_cls(is_async): 44 | if is_async: 45 | return AsyncRateLimiter 46 | else: 47 | return RateLimiter 48 | 49 | 50 | @pytest.fixture 51 | def mock_clock(rate_limiter_cls): 52 | with patch.object(rate_limiter_cls, '_clock') as mock_clock: 53 | yield mock_clock 54 | 55 | 56 | @pytest.fixture 57 | def mock_sleep(auto_async_side_effect, rate_limiter_cls): 58 | with patch.object(rate_limiter_cls, '_sleep') as mock_sleep: 59 | mock_sleep.side_effect = auto_async_side_effect(None) 60 | yield mock_sleep 61 | 62 | 63 | async def test_min_delay( 64 | rate_limiter_cls, mock_clock, mock_sleep, auto_async_side_effect, auto_async 65 | ): 66 | mock_func = MagicMock() 67 | mock_func.side_effect = auto_async_side_effect(None) 68 | min_delay = 3.5 69 | 70 | mock_clock.side_effect = [1] 71 | rl = rate_limiter_cls(mock_func, min_delay_seconds=min_delay) 72 | 73 | # First call -- no delay 74 | clock_first = 10 75 | mock_clock.side_effect = [clock_first, clock_first] # no delay here 76 | await auto_async(rl(sentinel.arg, kwa=sentinel.kwa)) 77 | mock_sleep.assert_not_called() 78 | mock_func.assert_called_once_with(sentinel.arg, kwa=sentinel.kwa) 79 | 80 | # Second call after min_delay/3 seconds -- should be delayed 81 | clock_second = clock_first + (min_delay / 3) 82 | mock_clock.side_effect = [clock_second, clock_first + min_delay] 83 | await auto_async(rl(sentinel.arg, kwa=sentinel.kwa)) 84 | mock_sleep.assert_called_with(min_delay - (clock_second - clock_first)) 85 | mock_sleep.reset_mock() 86 | 87 | # Third call after min_delay*2 seconds -- no delay again 88 | clock_third = clock_first + min_delay + min_delay * 2 89 | mock_clock.side_effect = [clock_third, clock_third] 90 | await auto_async(rl(sentinel.arg, kwa=sentinel.kwa)) 91 | mock_sleep.assert_not_called() 92 | 93 | 94 | async def test_max_retries( 95 | rate_limiter_cls, mock_clock, mock_sleep, auto_async_side_effect, auto_async 96 | ): 97 | mock_func = MagicMock() 98 | mock_clock.return_value = 1 99 | rl = rate_limiter_cls( 100 | mock_func, max_retries=3, 101 | return_value_on_exception=sentinel.return_value, 102 | ) 103 | 104 | # Non-geopy errors must not be swallowed 105 | mock_func.side_effect = auto_async_side_effect(ValueError) 106 | with pytest.raises(ValueError): 107 | await auto_async(rl(sentinel.arg)) 108 | assert 1 == mock_func.call_count 109 | mock_func.reset_mock() 110 | 111 | # geopy errors must be swallowed and retried 112 | mock_func.side_effect = auto_async_side_effect(GeocoderServiceError) 113 | assert sentinel.return_value == await auto_async(rl(sentinel.arg)) 114 | assert 4 == mock_func.call_count 115 | mock_func.reset_mock() 116 | 117 | # Successful value must be returned 118 | mock_func.side_effect = auto_async_side_effect(side_effect=[ 119 | GeocoderServiceError, GeocoderServiceError, sentinel.good 120 | ]) 121 | assert sentinel.good == await auto_async(rl(sentinel.arg)) 122 | assert 3 == mock_func.call_count 123 | mock_func.reset_mock() 124 | 125 | # When swallowing is disabled, the exception must be raised 126 | rl.swallow_exceptions = False 127 | mock_func.side_effect = auto_async_side_effect(GeocoderQuotaExceeded) 128 | with pytest.raises(GeocoderQuotaExceeded): 129 | await auto_async(rl(sentinel.arg)) 130 | assert 4 == mock_func.call_count 131 | mock_func.reset_mock() 132 | 133 | 134 | async def test_error_wait_seconds( 135 | rate_limiter_cls, mock_clock, mock_sleep, auto_async_side_effect, auto_async 136 | ): 137 | mock_func = MagicMock() 138 | error_wait = 3.3 139 | 140 | mock_clock.return_value = 1 141 | rl = rate_limiter_cls( 142 | mock_func, max_retries=3, 143 | error_wait_seconds=error_wait, 144 | return_value_on_exception=sentinel.return_value, 145 | ) 146 | 147 | mock_func.side_effect = auto_async_side_effect(GeocoderServiceError) 148 | assert sentinel.return_value == await auto_async(rl(sentinel.arg)) 149 | assert 4 == mock_func.call_count 150 | assert 3 == mock_sleep.call_count 151 | mock_sleep.assert_called_with(error_wait) 152 | mock_func.reset_mock() 153 | 154 | 155 | async def test_sync_raises_for_awaitable(): 156 | def g(): # non-async function returning an awaitable -- like `geocode`. 157 | async def coro(): 158 | pass # pragma: no cover 159 | 160 | # Make a task from the coroutine, to avoid 161 | # the `coroutine 'coro' was never awaited` warning: 162 | task = asyncio.ensure_future(coro()) 163 | return task 164 | 165 | rl = RateLimiter(g) 166 | 167 | with pytest.raises(ValueError): 168 | await rl() 169 | -------------------------------------------------------------------------------- /geopy/geocoders/geolake.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | from functools import partial 3 | from urllib.parse import urlencode 4 | 5 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 6 | from geopy.location import Location 7 | from geopy.util import join_filter, logger 8 | 9 | __all__ = ("Geolake", ) 10 | 11 | 12 | class Geolake(Geocoder): 13 | """Geocoder using the Geolake API. 14 | 15 | Documentation at: 16 | https://geolake.com/docs/api 17 | 18 | Terms of Service at: 19 | https://geolake.com/terms-of-use 20 | """ 21 | 22 | structured_query_params = { 23 | 'country', 24 | 'state', 25 | 'city', 26 | 'zipcode', 27 | 'street', 28 | 'address', 29 | 'houseNumber', 30 | 'subNumber', 31 | } 32 | 33 | api_path = '/v1/geocode' 34 | 35 | def __init__( 36 | self, 37 | api_key, 38 | *, 39 | domain='api.geolake.com', 40 | scheme=None, 41 | timeout=DEFAULT_SENTINEL, 42 | proxies=DEFAULT_SENTINEL, 43 | user_agent=None, 44 | ssl_context=DEFAULT_SENTINEL, 45 | adapter_factory=None 46 | ): 47 | """ 48 | 49 | :param str api_key: The API key required by Geolake 50 | to perform geocoding requests. You can get your key here: 51 | https://geolake.com/ 52 | 53 | :param str domain: Currently it is ``'api.geolake.com'``, can 54 | be changed for testing purposes. 55 | 56 | :param str scheme: 57 | See :attr:`geopy.geocoders.options.default_scheme`. 58 | 59 | :param int timeout: 60 | See :attr:`geopy.geocoders.options.default_timeout`. 61 | 62 | :param dict proxies: 63 | See :attr:`geopy.geocoders.options.default_proxies`. 64 | 65 | :param str user_agent: 66 | See :attr:`geopy.geocoders.options.default_user_agent`. 67 | 68 | :type ssl_context: :class:`ssl.SSLContext` 69 | :param ssl_context: 70 | See :attr:`geopy.geocoders.options.default_ssl_context`. 71 | 72 | :param callable adapter_factory: 73 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 74 | 75 | .. versionadded:: 2.0 76 | 77 | """ 78 | super().__init__( 79 | scheme=scheme, 80 | timeout=timeout, 81 | proxies=proxies, 82 | user_agent=user_agent, 83 | ssl_context=ssl_context, 84 | adapter_factory=adapter_factory, 85 | ) 86 | 87 | self.api_key = api_key 88 | self.domain = domain.strip('/') 89 | self.api = '%s://%s%s' % (self.scheme, self.domain, self.api_path) 90 | 91 | def geocode( 92 | self, 93 | query, 94 | *, 95 | country_codes=None, 96 | exactly_one=True, 97 | timeout=DEFAULT_SENTINEL 98 | ): 99 | """ 100 | Return a location point by address. 101 | 102 | :param query: The address or query you wish to geocode. 103 | 104 | For a structured query, provide a dictionary whose keys 105 | are one of: `country`, `state`, `city`, `zipcode`, `street`, `address`, 106 | `houseNumber` or `subNumber`. 107 | :type query: str or dict 108 | 109 | :param country_codes: Provides the geocoder with a list 110 | of country codes that the query may reside in. This value will 111 | limit the geocoder to the supplied countries. The country code 112 | is a 2 character code as defined by the ISO-3166-1 alpha-2 113 | standard (e.g. ``FR``). Multiple countries can be specified with 114 | a Python list. 115 | 116 | :type country_codes: str or list 117 | 118 | :param bool exactly_one: Return one result or a list of one result. 119 | 120 | :param int timeout: Time, in seconds, to wait for the geocoding service 121 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 122 | exception. Set this only if you wish to override, on this call 123 | only, the value set during the geocoder's initialization. 124 | 125 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 126 | ``exactly_one=False``. 127 | 128 | """ 129 | 130 | if isinstance(query, collections.abc.Mapping): 131 | params = { 132 | key: val 133 | for key, val 134 | in query.items() 135 | if key in self.structured_query_params 136 | } 137 | params['api_key'] = self.api_key 138 | else: 139 | params = { 140 | 'api_key': self.api_key, 141 | 'q': query, 142 | } 143 | 144 | if not country_codes: 145 | country_codes = [] 146 | if isinstance(country_codes, str): 147 | country_codes = [country_codes] 148 | if country_codes: 149 | params['countryCodes'] = ",".join(country_codes) 150 | 151 | url = "?".join((self.api, urlencode(params))) 152 | 153 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 154 | callback = partial(self._parse_json, exactly_one=exactly_one) 155 | return self._call_geocoder(url, callback, timeout=timeout) 156 | 157 | def _parse_json(self, page, exactly_one): 158 | """Returns location, (latitude, longitude) from json feed.""" 159 | 160 | if not page.get('success'): 161 | return None 162 | 163 | latitude = page['latitude'] 164 | longitude = page['longitude'] 165 | 166 | address = self._get_address(page) 167 | result = Location(address, (latitude, longitude), page) 168 | if exactly_one: 169 | return result 170 | else: 171 | return [result] 172 | 173 | def _get_address(self, page): 174 | """ 175 | Returns address string from page dictionary 176 | :param page: dict 177 | :return: str 178 | """ 179 | place = page.get('place') 180 | address_city = place.get('city') 181 | address_country_code = place.get('countryCode') 182 | address = join_filter(', ', [address_city, address_country_code]) 183 | return address 184 | -------------------------------------------------------------------------------- /geopy/geocoders/banfrance.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import urlencode 3 | 4 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 5 | from geopy.location import Location 6 | from geopy.util import logger 7 | 8 | __all__ = ("BANFrance", ) 9 | 10 | 11 | class BANFrance(Geocoder): 12 | """Geocoder using the Base Adresse Nationale France API. 13 | 14 | Documentation at: 15 | https://adresse.data.gouv.fr/api 16 | """ 17 | 18 | geocode_path = '/search' 19 | reverse_path = '/reverse' 20 | 21 | def __init__( 22 | self, 23 | *, 24 | domain='api-adresse.data.gouv.fr', 25 | scheme=None, 26 | timeout=DEFAULT_SENTINEL, 27 | proxies=DEFAULT_SENTINEL, 28 | user_agent=None, 29 | ssl_context=DEFAULT_SENTINEL, 30 | adapter_factory=None 31 | ): 32 | """ 33 | 34 | :param str domain: Currently it is ``'api-adresse.data.gouv.fr'``, can 35 | be changed for testing purposes. 36 | 37 | :param str scheme: 38 | See :attr:`geopy.geocoders.options.default_scheme`. 39 | 40 | :param int timeout: 41 | See :attr:`geopy.geocoders.options.default_timeout`. 42 | 43 | :param dict proxies: 44 | See :attr:`geopy.geocoders.options.default_proxies`. 45 | 46 | :param str user_agent: 47 | See :attr:`geopy.geocoders.options.default_user_agent`. 48 | 49 | :type ssl_context: :class:`ssl.SSLContext` 50 | :param ssl_context: 51 | See :attr:`geopy.geocoders.options.default_ssl_context`. 52 | 53 | :param callable adapter_factory: 54 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 55 | 56 | .. versionadded:: 2.0 57 | 58 | """ 59 | super().__init__( 60 | scheme=scheme, 61 | timeout=timeout, 62 | proxies=proxies, 63 | user_agent=user_agent, 64 | ssl_context=ssl_context, 65 | adapter_factory=adapter_factory, 66 | ) 67 | self.domain = domain.strip('/') 68 | 69 | self.geocode_api = ( 70 | '%s://%s%s' % (self.scheme, self.domain, self.geocode_path) 71 | ) 72 | self.reverse_api = ( 73 | '%s://%s%s' % (self.scheme, self.domain, self.reverse_path) 74 | ) 75 | 76 | def geocode( 77 | self, 78 | query, 79 | *, 80 | limit=None, 81 | exactly_one=True, 82 | timeout=DEFAULT_SENTINEL 83 | ): 84 | """ 85 | Return a location point by address. 86 | 87 | :param str query: The address or query you wish to geocode. 88 | 89 | :param int limit: Defines the maximum number of items in the 90 | response structure. If not provided and there are multiple 91 | results the BAN API will return 5 results by default. 92 | This will be reset to one if ``exactly_one`` is True. 93 | 94 | :param bool exactly_one: Return one result or a list of results, if 95 | available. 96 | 97 | :param int timeout: Time, in seconds, to wait for the geocoding service 98 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 99 | exception. Set this only if you wish to override, on this call 100 | only, the value set during the geocoder's initialization. 101 | 102 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 103 | ``exactly_one=False``. 104 | 105 | """ 106 | 107 | params = { 108 | 'q': query, 109 | } 110 | 111 | if limit is not None: 112 | params['limit'] = limit 113 | 114 | url = "?".join((self.geocode_api, urlencode(params))) 115 | 116 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 117 | callback = partial(self._parse_json, exactly_one=exactly_one) 118 | return self._call_geocoder(url, callback, timeout=timeout) 119 | 120 | def reverse( 121 | self, 122 | query, 123 | *, 124 | exactly_one=True, 125 | timeout=DEFAULT_SENTINEL 126 | ): 127 | """ 128 | Return an address by location point. 129 | 130 | :param query: The coordinates for which you wish to obtain the 131 | closest human-readable addresses. 132 | :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, 133 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 134 | 135 | :param bool exactly_one: Return one result or a list of results, if 136 | available. 137 | 138 | :param int timeout: Time, in seconds, to wait for the geocoding service 139 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 140 | exception. Set this only if you wish to override, on this call 141 | only, the value set during the geocoder's initialization. 142 | 143 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 144 | ``exactly_one=False``. 145 | 146 | """ 147 | 148 | try: 149 | lat, lng = self._coerce_point_to_string(query).split(',') 150 | except ValueError: 151 | raise ValueError("Must be a coordinate pair or Point") 152 | 153 | params = { 154 | 'lat': lat, 155 | 'lng': lng, 156 | } 157 | 158 | url = "?".join((self.reverse_api, urlencode(params))) 159 | logger.debug("%s.reverse: %s", self.__class__.__name__, url) 160 | callback = partial(self._parse_json, exactly_one=exactly_one) 161 | return self._call_geocoder(url, callback, timeout=timeout) 162 | 163 | def _parse_feature(self, feature): 164 | # Parse each resource. 165 | latitude = feature.get('geometry', {}).get('coordinates', [])[1] 166 | longitude = feature.get('geometry', {}).get('coordinates', [])[0] 167 | placename = feature.get('properties', {}).get('label') 168 | 169 | return Location(placename, (latitude, longitude), feature) 170 | 171 | def _parse_json(self, response, exactly_one): 172 | if response is None or 'features' not in response: 173 | return None 174 | features = response['features'] 175 | if not len(features): 176 | return None 177 | if exactly_one: 178 | return self._parse_feature(features[0]) 179 | else: 180 | return [self._parse_feature(feature) for feature in features] 181 | -------------------------------------------------------------------------------- /test/geocoders/geonames.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | 5 | from geopy import Point 6 | from geopy.exc import GeocoderAuthenticationFailure, GeocoderQueryError 7 | from geopy.geocoders import GeoNames 8 | from test.geocoders.util import BaseTestGeocoder, env 9 | 10 | try: 11 | import pytz 12 | pytz_available = True 13 | except ImportError: 14 | pytz_available = False 15 | 16 | 17 | class TestUnitGeoNames: 18 | 19 | def test_user_agent_custom(self): 20 | geocoder = GeoNames( 21 | username='DUMMYUSER_NORBERT', 22 | user_agent='my_user_agent/1.0' 23 | ) 24 | assert geocoder.headers['User-Agent'] == 'my_user_agent/1.0' 25 | 26 | 27 | class TestGeoNames(BaseTestGeocoder): 28 | 29 | delta = 0.04 30 | 31 | @classmethod 32 | def make_geocoder(cls, **kwargs): 33 | return GeoNames(username=env['GEONAMES_USERNAME'], **kwargs) 34 | 35 | async def test_geocode(self): 36 | await self.geocode_run( 37 | {"query": "Mount Everest, Nepal"}, 38 | {"latitude": 27.987, "longitude": 86.925}, 39 | ) 40 | 41 | async def test_query_urlencoding(self): 42 | location = await self.geocode_run( 43 | {"query": "Ry\u016b\u014d"}, 44 | {"latitude": 35.65, "longitude": 138.5}, 45 | ) 46 | assert "Ry\u016b\u014d" in location.address 47 | 48 | async def test_reverse(self): 49 | location = await self.reverse_run( 50 | { 51 | "query": "40.75376406311989, -73.98489005863667", 52 | }, 53 | { 54 | "latitude": 40.75376406311989, 55 | "longitude": -73.98489005863667, 56 | }, 57 | ) 58 | assert "Times Square" in location.address 59 | 60 | async def test_geocode_empty_response(self): 61 | await self.geocode_run( 62 | {"query": "sdlahaslkhdkasldhkjsahdlkash"}, 63 | {}, 64 | expect_failure=True, 65 | ) 66 | 67 | async def test_reverse_nearby_place_name_raises_for_feature_code(self): 68 | with pytest.raises(ValueError): 69 | await self.reverse_run( 70 | { 71 | "query": "40.75376406311989, -73.98489005863667", 72 | "feature_code": "ADM1", 73 | }, 74 | {}, 75 | ) 76 | 77 | with pytest.raises(ValueError): 78 | await self.reverse_run( 79 | { 80 | "query": "40.75376406311989, -73.98489005863667", 81 | "feature_code": "ADM1", 82 | "find_nearby_type": "findNearbyPlaceName", 83 | }, 84 | {}, 85 | ) 86 | 87 | async def test_reverse_nearby_place_name_lang(self): 88 | location = await self.reverse_run( 89 | { 90 | "query": "52.50, 13.41", 91 | "lang": 'ru', 92 | }, 93 | {}, 94 | ) 95 | assert 'Берлин, Германия' in location.address 96 | 97 | async def test_reverse_find_nearby_raises_for_lang(self): 98 | with pytest.raises(ValueError): 99 | await self.reverse_run( 100 | { 101 | "query": "40.75376406311989, -73.98489005863667", 102 | "find_nearby_type": 'findNearby', 103 | "lang": 'en', 104 | }, 105 | {}, 106 | ) 107 | 108 | async def test_reverse_find_nearby(self): 109 | location = await self.reverse_run( 110 | { 111 | "query": "40.75376406311989, -73.98489005863667", 112 | "find_nearby_type": 'findNearby', 113 | }, 114 | { 115 | "latitude": 40.75376406311989, 116 | "longitude": -73.98489005863667, 117 | }, 118 | ) 119 | assert "New York, United States" in location.address 120 | 121 | async def test_reverse_find_nearby_feature_code(self): 122 | await self.reverse_run( 123 | { 124 | "query": "40.75376406311989, -73.98489005863667", 125 | "find_nearby_type": 'findNearby', 126 | "feature_code": "ADM1", 127 | }, 128 | { 129 | "latitude": 40.16706, 130 | "longitude": -74.49987, 131 | }, 132 | ) 133 | 134 | async def test_reverse_raises_for_unknown_find_nearby_type(self): 135 | with pytest.raises(GeocoderQueryError): 136 | await self.reverse_run( 137 | { 138 | "query": "40.75376406311989, -73.98489005863667", 139 | "find_nearby_type": "findSomethingNonExisting", 140 | }, 141 | {}, 142 | ) 143 | 144 | @pytest.mark.skipif("not pytz_available") 145 | async def test_reverse_timezone(self): 146 | new_york_point = Point(40.75376406311989, -73.98489005863667) 147 | america_new_york = pytz.timezone("America/New_York") 148 | 149 | timezone = await self.reverse_timezone_run( 150 | {"query": new_york_point}, 151 | america_new_york, 152 | ) 153 | assert timezone.raw['countryCode'] == 'US' 154 | 155 | @pytest.mark.skipif("not pytz_available") 156 | async def test_reverse_timezone_unknown(self): 157 | await self.reverse_timezone_run( 158 | # Geonames doesn't return `timezoneId` for Antarctica, 159 | # but it provides GMT offset which can be used 160 | # to create a FixedOffset pytz timezone. 161 | {"query": "89.0, 1.0"}, 162 | pytz.UTC, 163 | ) 164 | await self.reverse_timezone_run( 165 | {"query": "89.0, 80.0"}, 166 | pytz.FixedOffset(5 * 60), 167 | ) 168 | 169 | async def test_country_str(self): 170 | await self.geocode_run( 171 | {"query": "kazan", "country": "TR"}, 172 | {"latitude": 40.2317, "longitude": 32.6839}, 173 | ) 174 | 175 | async def test_country_list(self): 176 | await self.geocode_run( 177 | {"query": "kazan", "country": ["CN", "TR", "JP"]}, 178 | {"latitude": 40.2317, "longitude": 32.6839}, 179 | ) 180 | 181 | async def test_country_bias(self): 182 | await self.geocode_run( 183 | {"query": "kazan", "country_bias": "TR"}, 184 | {"latitude": 40.2317, "longitude": 32.6839}, 185 | ) 186 | 187 | 188 | class TestGeoNamesInvalidAccount(BaseTestGeocoder): 189 | 190 | @classmethod 191 | def make_geocoder(cls, **kwargs): 192 | return GeoNames( 193 | username="geopy-not-existing-%s" % uuid.uuid4(), 194 | **kwargs 195 | ) 196 | 197 | async def test_geocode(self): 198 | with pytest.raises(GeocoderAuthenticationFailure): 199 | await self.geocode_run( 200 | {"query": "moscow"}, 201 | {}, 202 | expect_failure=True, 203 | ) 204 | 205 | @pytest.mark.skipif("not pytz_available") 206 | async def test_reverse_timezone(self): 207 | with pytest.raises(GeocoderAuthenticationFailure): 208 | await self.reverse_timezone_run( 209 | {"query": "40.6997716, -73.9753359"}, 210 | None, 211 | ) 212 | -------------------------------------------------------------------------------- /geopy/geocoders/mapbox.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import quote, urlencode 3 | 4 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 5 | from geopy.location import Location 6 | from geopy.point import Point 7 | from geopy.util import logger 8 | 9 | __all__ = ("MapBox", ) 10 | 11 | 12 | class MapBox(Geocoder): 13 | """Geocoder using the Mapbox API. 14 | 15 | Documentation at: 16 | https://www.mapbox.com/api-documentation/ 17 | """ 18 | 19 | api_path = '/geocoding/v5/mapbox.places/%(query)s.json/' 20 | 21 | def __init__( 22 | self, 23 | api_key, 24 | *, 25 | scheme=None, 26 | timeout=DEFAULT_SENTINEL, 27 | proxies=DEFAULT_SENTINEL, 28 | user_agent=None, 29 | ssl_context=DEFAULT_SENTINEL, 30 | adapter_factory=None, 31 | domain='api.mapbox.com' 32 | ): 33 | """ 34 | :param str api_key: The API key required by Mapbox to perform 35 | geocoding requests. API keys are managed through Mapox's account 36 | page (https://www.mapbox.com/account/access-tokens). 37 | 38 | :param str scheme: 39 | See :attr:`geopy.geocoders.options.default_scheme`. 40 | 41 | :param int timeout: 42 | See :attr:`geopy.geocoders.options.default_timeout`. 43 | 44 | :param dict proxies: 45 | See :attr:`geopy.geocoders.options.default_proxies`. 46 | 47 | :param str user_agent: 48 | See :attr:`geopy.geocoders.options.default_user_agent`. 49 | 50 | :type ssl_context: :class:`ssl.SSLContext` 51 | :param ssl_context: 52 | See :attr:`geopy.geocoders.options.default_ssl_context`. 53 | 54 | :param callable adapter_factory: 55 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 56 | 57 | .. versionadded:: 2.0 58 | 59 | :param str domain: base api domain for mapbox 60 | """ 61 | super().__init__( 62 | scheme=scheme, 63 | timeout=timeout, 64 | proxies=proxies, 65 | user_agent=user_agent, 66 | ssl_context=ssl_context, 67 | adapter_factory=adapter_factory, 68 | ) 69 | self.api_key = api_key 70 | self.domain = domain.strip('/') 71 | self.api = "%s://%s%s" % (self.scheme, self.domain, self.api_path) 72 | 73 | def _parse_json(self, json, exactly_one=True): 74 | '''Returns location, (latitude, longitude) from json feed.''' 75 | features = json['features'] 76 | if features == []: 77 | return None 78 | 79 | def parse_feature(feature): 80 | location = feature['place_name'] 81 | longitude = feature['geometry']['coordinates'][0] 82 | latitude = feature['geometry']['coordinates'][1] 83 | return Location(location, (latitude, longitude), feature) 84 | if exactly_one: 85 | return parse_feature(features[0]) 86 | else: 87 | return [parse_feature(feature) for feature in features] 88 | 89 | def geocode( 90 | self, 91 | query, 92 | *, 93 | exactly_one=True, 94 | timeout=DEFAULT_SENTINEL, 95 | proximity=None, 96 | country=None, 97 | bbox=None 98 | ): 99 | """ 100 | Return a location point by address. 101 | 102 | :param str query: The address or query you wish to geocode. 103 | 104 | :param bool exactly_one: Return one result or a list of results, if 105 | available. 106 | 107 | :param int timeout: Time, in seconds, to wait for the geocoding service 108 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 109 | exception. Set this only if you wish to override, on this call 110 | only, the value set during the geocoder's initialization. 111 | 112 | :param proximity: A coordinate to bias local results based on a provided 113 | location. 114 | :type proximity: :class:`geopy.point.Point`, list or tuple of ``(latitude, 115 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 116 | 117 | :param country: Country to filter result in form of 118 | ISO 3166-1 alpha-2 country code (e.g. ``FR``). 119 | Might be a Python list of strings. 120 | 121 | :type country: str or list 122 | 123 | :param bbox: The bounding box of the viewport within which 124 | to bias geocode results more prominently. 125 | Example: ``[Point(22, 180), Point(-22, -180)]``. 126 | :type bbox: list or tuple of 2 items of :class:`geopy.point.Point` or 127 | ``(latitude, longitude)`` or ``"%(latitude)s, %(longitude)s"``. 128 | 129 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 130 | ``exactly_one=False``. 131 | """ 132 | params = {} 133 | 134 | params['access_token'] = self.api_key 135 | if bbox: 136 | params['bbox'] = self._format_bounding_box( 137 | bbox, "%(lon1)s,%(lat1)s,%(lon2)s,%(lat2)s") 138 | 139 | if not country: 140 | country = [] 141 | if isinstance(country, str): 142 | country = [country] 143 | if country: 144 | params['country'] = ",".join(country) 145 | 146 | if proximity: 147 | p = Point(proximity) 148 | params['proximity'] = "%s,%s" % (p.longitude, p.latitude) 149 | 150 | quoted_query = quote(query.encode('utf-8')) 151 | url = "?".join((self.api % dict(query=quoted_query), 152 | urlencode(params))) 153 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 154 | callback = partial(self._parse_json, exactly_one=exactly_one) 155 | return self._call_geocoder(url, callback, timeout=timeout) 156 | 157 | def reverse( 158 | self, 159 | query, 160 | *, 161 | exactly_one=True, 162 | timeout=DEFAULT_SENTINEL 163 | ): 164 | """ 165 | Return an address by location point. 166 | 167 | :param query: The coordinates for which you wish to obtain the 168 | closest human-readable addresses. 169 | :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, 170 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 171 | 172 | :param bool exactly_one: Return one result or a list of results, if 173 | available. 174 | 175 | :param int timeout: Time, in seconds, to wait for the geocoding service 176 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 177 | exception. Set this only if you wish to override, on this call 178 | only, the value set during the geocoder's initialization. 179 | 180 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 181 | ``exactly_one=False``. 182 | """ 183 | params = {} 184 | params['access_token'] = self.api_key 185 | 186 | point = self._coerce_point_to_string(query, "%(lon)s,%(lat)s") 187 | quoted_query = quote(point.encode('utf-8')) 188 | url = "?".join((self.api % dict(query=quoted_query), 189 | urlencode(params))) 190 | logger.debug("%s.reverse: %s", self.__class__.__name__, url) 191 | callback = partial(self._parse_json, exactly_one=exactly_one) 192 | return self._call_geocoder(url, callback, timeout=timeout) 193 | -------------------------------------------------------------------------------- /test/geocoders/util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from abc import ABC, abstractmethod 4 | from unittest.mock import ANY, patch 5 | 6 | import pytest 7 | from async_generator import async_generator, asynccontextmanager, yield_ 8 | 9 | from geopy import exc 10 | from geopy.adapters import BaseAsyncAdapter 11 | from geopy.location import Location 12 | 13 | _env = {} 14 | try: 15 | with open(".test_keys") as fp: 16 | _env.update(json.loads(fp.read())) 17 | except IOError: 18 | _env.update(os.environ) 19 | 20 | 21 | class SkipIfMissingEnv(dict): 22 | def __init__(self, env): 23 | super().__init__(env) 24 | self.is_internet_access_allowed = None 25 | 26 | def __getitem__(self, key): 27 | assert self.is_internet_access_allowed is not None 28 | if key not in self: 29 | if self.is_internet_access_allowed: 30 | pytest.skip("Missing geocoder credential: %s" % (key,)) 31 | else: 32 | # Generate some dummy token. We won't perform a networking 33 | # request anyways. 34 | return "dummy" 35 | return super().__getitem__(key) 36 | 37 | 38 | env = SkipIfMissingEnv(_env) 39 | 40 | 41 | class BaseTestGeocoder(ABC): 42 | """ 43 | Base for geocoder-specific test cases. 44 | """ 45 | 46 | geocoder = None 47 | delta = 0.5 48 | 49 | @pytest.fixture(scope='class', autouse=True) 50 | @async_generator 51 | async def class_geocoder(_, request, patch_adapter, is_internet_access_allowed): 52 | """Prepare a class-level Geocoder instance.""" 53 | cls = request.cls 54 | env.is_internet_access_allowed = is_internet_access_allowed 55 | 56 | geocoder = cls.make_geocoder() 57 | cls.geocoder = geocoder 58 | 59 | run_async = isinstance(geocoder.adapter, BaseAsyncAdapter) 60 | if run_async: 61 | async with geocoder: 62 | await yield_(geocoder) 63 | else: 64 | await yield_(geocoder) 65 | 66 | @classmethod 67 | @asynccontextmanager 68 | @async_generator 69 | async def inject_geocoder(cls, geocoder): 70 | """An async context manager allowing to inject a custom 71 | geocoder instance in a single test method which will 72 | be used by the `geocode_run`/`reverse_run` methods. 73 | """ 74 | with patch.object(cls, 'geocoder', geocoder): 75 | run_async = isinstance(geocoder.adapter, BaseAsyncAdapter) 76 | if run_async: 77 | async with geocoder: 78 | await yield_(geocoder) 79 | else: 80 | await yield_(geocoder) 81 | 82 | @pytest.fixture(autouse=True) 83 | def ensure_no_geocoder_assignment(self): 84 | yield 85 | assert self.geocoder is type(self).geocoder, ( 86 | "Detected `self.geocoder` assignment. " 87 | "Please use `async with inject_geocoder(my_geocoder):` " 88 | "instead, which supports async adapters." 89 | ) 90 | 91 | @classmethod 92 | @abstractmethod 93 | def make_geocoder(cls, **kwargs): # pragma: no cover 94 | pass 95 | 96 | async def geocode_run( 97 | self, payload, expected, 98 | *, 99 | skiptest_on_errors=True, 100 | expect_failure=False 101 | ): 102 | """ 103 | Calls geocoder.geocode(**payload), then checks against `expected`. 104 | """ 105 | cls = type(self) 106 | result = await self._make_request( 107 | self.geocoder, 'geocode', 108 | skiptest_on_errors=skiptest_on_errors, 109 | **payload, 110 | ) 111 | if expect_failure: 112 | assert result is None 113 | return 114 | if result is None: 115 | pytest.fail('%s: No result found' % cls.__name__) 116 | if result == []: 117 | pytest.fail('%s returned an empty list instead of None' % cls.__name__) 118 | self._verify_request(result, exactly_one=payload.get('exactly_one', True), 119 | **expected) 120 | return result 121 | 122 | async def reverse_run( 123 | self, payload, expected, 124 | *, 125 | skiptest_on_errors=True, 126 | expect_failure=False 127 | ): 128 | """ 129 | Calls geocoder.reverse(**payload), then checks against `expected`. 130 | """ 131 | cls = type(self) 132 | result = await self._make_request( 133 | self.geocoder, 'reverse', 134 | skiptest_on_errors=skiptest_on_errors, 135 | **payload, 136 | ) 137 | if expect_failure: 138 | assert result is None 139 | return 140 | if result is None: 141 | pytest.fail('%s: No result found' % cls.__name__) 142 | if result == []: 143 | pytest.fail('%s returned an empty list instead of None' % cls.__name__) 144 | self._verify_request(result, exactly_one=payload.get('exactly_one', True), 145 | **expected) 146 | return result 147 | 148 | async def reverse_timezone_run(self, payload, expected, *, skiptest_on_errors=True): 149 | timezone = await self._make_request( 150 | self.geocoder, 'reverse_timezone', 151 | skiptest_on_errors=skiptest_on_errors, 152 | **payload, 153 | ) 154 | if expected is None: 155 | assert timezone is None 156 | else: 157 | assert timezone.pytz_timezone == expected 158 | 159 | return timezone 160 | 161 | async def _make_request(self, geocoder, method, *, skiptest_on_errors, **kwargs): 162 | cls = type(self) 163 | call = getattr(geocoder, method) 164 | run_async = isinstance(geocoder.adapter, BaseAsyncAdapter) 165 | try: 166 | if run_async: 167 | result = await call(**kwargs) 168 | else: 169 | result = call(**kwargs) 170 | except exc.GeocoderRateLimited as e: 171 | if not skiptest_on_errors: 172 | raise 173 | pytest.skip( 174 | "%s: Rate-limited, retry-after %s" % (cls.__name__, e.retry_after) 175 | ) 176 | except exc.GeocoderQuotaExceeded: 177 | if not skiptest_on_errors: 178 | raise 179 | pytest.skip("%s: Quota exceeded" % cls.__name__) 180 | except exc.GeocoderTimedOut: 181 | if not skiptest_on_errors: 182 | raise 183 | pytest.skip("%s: Service timed out" % cls.__name__) 184 | except exc.GeocoderUnavailable: 185 | if not skiptest_on_errors: 186 | raise 187 | pytest.skip("%s: Service unavailable" % cls.__name__) 188 | return result 189 | 190 | def _verify_request( 191 | self, 192 | result, 193 | latitude=ANY, 194 | longitude=ANY, 195 | address=ANY, 196 | exactly_one=True, 197 | delta=None, 198 | ): 199 | if exactly_one: 200 | assert isinstance(result, Location) 201 | else: 202 | assert isinstance(result, list) 203 | 204 | item = result if exactly_one else result[0] 205 | delta = delta or self.delta 206 | 207 | expected = ( 208 | pytest.approx(latitude, abs=delta) if latitude is not ANY else ANY, 209 | pytest.approx(longitude, abs=delta) if longitude is not ANY else ANY, 210 | address, 211 | ) 212 | received = ( 213 | item.latitude, 214 | item.longitude, 215 | item.address, 216 | ) 217 | assert received == expected 218 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to geopy 2 | 3 | ## Reporting issues 4 | 5 | If you caught an exception from geopy please try to Google the error first. 6 | There is a great chance that it has already been discussed somewhere 7 | and solutions have been provided (usually on GitHub or on Stack Overflow). 8 | 9 | Before reporting an issue please ensure that you have tried 10 | to get the answer from the doc: https://geopy.readthedocs.io/. 11 | 12 | Keep in mind that if a specific geocoding service's API is not behaving 13 | correctly then it probably won't be helpful to report that issue 14 | here, see https://geopy.readthedocs.io/en/latest/#geopy-is-not-a-service 15 | 16 | The following resources are available for your input: 17 | 18 | 1. Stack Overflow with [geopy tag](https://stackoverflow.com/questions/tagged/geopy). 19 | There's a somewhat active community here so you will probably get 20 | a solution quicker. And also there is a large amount of already 21 | resolved questions which can help too! Just remember to put the `geopy` 22 | tag if you'd decide to open a question. 23 | 1. [GitHub Discussions](https://github.com/geopy/geopy/discussions) is 24 | a good place to start if Stack Overflow didn't help or you have 25 | some idea or a feature request you'd like to bring up, or if you 26 | just have trouble and not sure you're doing everything right. 27 | Solutions and helpful snippets/patterns are also very welcome here. 28 | 1. [GitHub Issues](https://github.com/geopy/geopy/issues) should only 29 | be used for definite bug reports and specific tasks. If you're not sure 30 | whether your issue fits this then please start with Discussions 31 | first. 32 | 33 | 34 | ## Submitting patches 35 | 36 | If you contribute code to geopy, you agree to license your code under the MIT. 37 | 38 | The new code should follow [PEP8](https://pep8.org/) coding style (except 39 | the line length limit, which is 90) and adhere to the style of 40 | the surrounding code. 41 | 42 | You must document any functionality using Sphinx-compatible RST, and 43 | implement tests for any functionality in the `test` directory. 44 | 45 | In your Pull Requests there's no need to fill the changelog or AUTHORS, 46 | this is a maintainer's responsibility. 47 | 48 | For your convenience the contributing-friendly issues are marked with 49 | `help wanted` label, and the beginner-friendly ones with 50 | `good first issue`. 51 | 52 | If your PR remains unreviewed for a while, feel free to bug the maintainer. 53 | 54 | 55 | ### Setup 56 | 57 | 1. Create a virtualenv 58 | 2. Install `geopy` in editable mode along with dev dependencies: 59 | 60 | pip install -e ".[dev]" 61 | 62 | 3. Ensure that tests pass 63 | 64 | make test 65 | 66 | 67 | ### Running tests 68 | 69 | To quickly run the test suite without Internet access: 70 | 71 | make test-local 72 | 73 | To run the full test suite (makes queries to the geocoding services): 74 | 75 | make test 76 | 77 | Or simply: 78 | 79 | pytest 80 | 81 | To run a specific test module, pass a path as an argument to pytest. 82 | For example: 83 | 84 | pytest test/geocoders/nominatim.py 85 | 86 | Before pushing your code, make sure that linting passes, otherwise a CI 87 | build would fail: 88 | 89 | make lint 90 | 91 | 92 | ### Geocoder credentials 93 | 94 | Some geocoders require credentials (like API keys) for testing. They must 95 | remain secret, so if you need to test a code which requires them, you should 96 | obtain your own valid credentials. 97 | 98 | Tests in CI from forks and PRs run in `test-local` mode only, i.e. no network 99 | requests are performed. Full test suite with network requests is run only 100 | for pushes to branches by maintainers. This 101 | helps to reduce load on the geocoding services and save some quotas associated 102 | with the credentials used by geopy. It means that PR builds won't actually test 103 | network requests. Code changing a geocoder should be tested locally. 104 | But it's acceptable to not test such code if obtaining the required credentials 105 | seems problematic: just leave a note 106 | so the maintainers would be aware that the code hasn't been tested. 107 | 108 | You may wonder: why not commit captured data and run mocked tests? 109 | Because there are copyright constraints on the data returned by services. 110 | Another reason is that this data goes obsolete quite fast, and maintaining 111 | it is cumbersome. 112 | 113 | To ease local testing the credentials can be stored in a json format 114 | in a file called `.test_keys` located at the root of the project 115 | instead of env variables. 116 | 117 | Example contents of `.test_keys`: 118 | 119 | { 120 | "BING_KEY": "...secret key...", 121 | "OPENCAGE_KEY": "...secret key..." 122 | } 123 | 124 | 125 | ### Building docs 126 | 127 | make docs 128 | 129 | Open `docs/_build/html/index.html` with a browser to see the docs. On macOS you 130 | can use the following command for that: 131 | 132 | open docs/_build/html/index.html 133 | 134 | 135 | ### Adding a new geocoder 136 | 137 | Patches for adding new geocoding services are very welcome! It doesn't matter 138 | how popular the target service is or whether its territorial coverage is 139 | global or local. 140 | 141 | A checklist for adding a new geocoder: 142 | 143 | 1. Create a new geocoding class in its own Python module in the 144 | `geopy/geocoders` package. Please look around to make sure that you're 145 | not reimplementing something that's already there! For example, if you're 146 | adding a Nominatim-based service, then your new geocoder class should 147 | probably extend the `geopy.geocoders.Nominatim` class. 148 | 149 | 2. Follow the instructions in the `geopy/geocoders/__init__.py` module for 150 | adding the required imports. 151 | 152 | 3. Create a test module in the `test/geocoders` directory. If your geocoder 153 | class requires credentials, make sure to access them via 154 | the `test.geocoders.util.env` object 155 | (see `test/geocoders/what3words.py` for example). 156 | Refer to the [Geocoder credentials](#geocoder-credentials) section 157 | above for info on how to work with credentials locally. 158 | 159 | 4. Complete your implementation and tests! Give TDD a try if you aren't used 160 | to it yet! 🎉 Please note that it's possible to run a single test module 161 | instead of running the full suite, which is much slower. Refer to the 162 | [Running tests](#running-tests) section above for a command example. 163 | 164 | 5. Make sure to document the `geocode` and `reverse` methods with all their 165 | parameters. The class doc should contain a URI to the Terms of Service. 166 | 167 | 6. Add a reference to that class in the `docs/index.rst` file. Please keep 168 | them ordered alphabetically by their module file names. Build the docs 169 | ([Building docs](#building-docs) section above) to make sure that you've 170 | done them right! 171 | 172 | 7. If your tests use credentials, add their names to 173 | the end of the `.github/workflows/ci.yml` file. 174 | 175 | That's all! 176 | 177 | ### Improving a geocoder 178 | 179 | If you want to add additional parameters to a `geocode` or `reverse` 180 | method, the additional parameters must be explicitly specified and documented 181 | in the method signature. Validation may be done for type, but values should 182 | probably not be checked against an enumerated list because the service could 183 | change. The additional parameters should go to the end of the method signature. 184 | 185 | Please avoid simply passing through arbitrary parameters 186 | (e.g. with `**kwargs`) to the params of a request to the target service. 187 | The target service parameters might change, as well as the service's API, 188 | but the geocoder class's public API should stay the same. It's almost 189 | impossible to achieve that when a pass-through of arbitrary parameters is 190 | allowed. 191 | 192 | -------------------------------------------------------------------------------- /geopy/geocoders/maptiler.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import quote, urlencode 3 | 4 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 5 | from geopy.location import Location 6 | from geopy.point import Point 7 | from geopy.util import logger 8 | 9 | __all__ = ("MapTiler", ) 10 | 11 | 12 | class MapTiler(Geocoder): 13 | """Geocoder using the MapTiler API. 14 | 15 | Documentation at: 16 | https://cloud.maptiler.com/geocoding/ (requires sign-up) 17 | """ 18 | 19 | api_path = '/geocoding/%(query)s.json' 20 | 21 | def __init__( 22 | self, 23 | api_key, 24 | *, 25 | scheme=None, 26 | timeout=DEFAULT_SENTINEL, 27 | proxies=DEFAULT_SENTINEL, 28 | user_agent=None, 29 | ssl_context=DEFAULT_SENTINEL, 30 | adapter_factory=None, 31 | domain='api.maptiler.com' 32 | ): 33 | """ 34 | :param str api_key: The API key required by Maptiler to perform 35 | geocoding requests. API keys are managed through Maptiler's account 36 | page (https://cloud.maptiler.com/account/keys). 37 | 38 | :param str scheme: 39 | See :attr:`geopy.geocoders.options.default_scheme`. 40 | 41 | :param int timeout: 42 | See :attr:`geopy.geocoders.options.default_timeout`. 43 | 44 | :param dict proxies: 45 | See :attr:`geopy.geocoders.options.default_proxies`. 46 | 47 | :param str user_agent: 48 | See :attr:`geopy.geocoders.options.default_user_agent`. 49 | 50 | :type ssl_context: :class:`ssl.SSLContext` 51 | :param ssl_context: 52 | See :attr:`geopy.geocoders.options.default_ssl_context`. 53 | 54 | :param callable adapter_factory: 55 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 56 | 57 | .. versionadded:: 2.0 58 | 59 | :param str domain: base api domain for Maptiler 60 | """ 61 | super().__init__( 62 | scheme=scheme, 63 | timeout=timeout, 64 | proxies=proxies, 65 | user_agent=user_agent, 66 | ssl_context=ssl_context, 67 | adapter_factory=adapter_factory, 68 | ) 69 | self.api_key = api_key 70 | self.domain = domain.strip('/') 71 | self.api = "%s://%s%s" % (self.scheme, self.domain, self.api_path) 72 | 73 | def _parse_json(self, json, exactly_one=True): 74 | # Returns location, (latitude, longitude) from json feed. 75 | features = json['features'] 76 | if not features: 77 | return None 78 | 79 | def parse_feature(feature): 80 | location = feature['place_name'] 81 | longitude = feature['center'][0] 82 | latitude = feature['center'][1] 83 | 84 | return Location(location, (latitude, longitude), feature) 85 | if exactly_one: 86 | return parse_feature(features[0]) 87 | else: 88 | return [parse_feature(feature) for feature in features] 89 | 90 | def geocode( 91 | self, 92 | query, 93 | *, 94 | exactly_one=True, 95 | timeout=DEFAULT_SENTINEL, 96 | proximity=None, 97 | language=None, 98 | bbox=None 99 | ): 100 | """ 101 | Return a location point by address. 102 | 103 | :param str query: The address or query you wish to geocode. 104 | 105 | :param bool exactly_one: Return one result or a list of results, if 106 | available. 107 | 108 | :param int timeout: Time, in seconds, to wait for the geocoding service 109 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 110 | exception. Set this only if you wish to override, on this call 111 | only, the value set during the geocoder's initialization. 112 | 113 | :param proximity: A coordinate to bias local results based on a provided 114 | location. 115 | :type proximity: :class:`geopy.point.Point`, list or tuple of ``(latitude, 116 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 117 | 118 | :param language: Prefer results in specific languages. Accepts 119 | a single string like ``"en"`` or a list like ``["de", "en"]``. 120 | :type language: str or list 121 | 122 | :param bbox: The bounding box of the viewport within which 123 | to bias geocode results more prominently. 124 | Example: ``[Point(22, 180), Point(-22, -180)]``. 125 | :type bbox: list or tuple of 2 items of :class:`geopy.point.Point` or 126 | ``(latitude, longitude)`` or ``"%(latitude)s, %(longitude)s"``. 127 | 128 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 129 | ``exactly_one=False``. 130 | """ 131 | params = {'key': self.api_key} 132 | 133 | query = query 134 | if bbox: 135 | params['bbox'] = self._format_bounding_box( 136 | bbox, "%(lon1)s,%(lat1)s,%(lon2)s,%(lat2)s") 137 | 138 | if isinstance(language, str): 139 | language = [language] 140 | if language: 141 | params['language'] = ','.join(language) 142 | 143 | if proximity: 144 | p = Point(proximity) 145 | params['proximity'] = "%s,%s" % (p.longitude, p.latitude) 146 | 147 | quoted_query = quote(query.encode('utf-8')) 148 | url = "?".join((self.api % dict(query=quoted_query), 149 | urlencode(params))) 150 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 151 | callback = partial(self._parse_json, exactly_one=exactly_one) 152 | return self._call_geocoder(url, callback, timeout=timeout) 153 | 154 | def reverse( 155 | self, 156 | query, 157 | *, 158 | exactly_one=True, 159 | timeout=DEFAULT_SENTINEL, 160 | language=None 161 | ): 162 | """ 163 | Return an address by location point. 164 | 165 | :param query: The coordinates for which you wish to obtain the 166 | closest human-readable addresses. 167 | :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, 168 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 169 | 170 | :param bool exactly_one: Return one result or a list of results, if 171 | available. 172 | 173 | :param int timeout: Time, in seconds, to wait for the geocoding service 174 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 175 | exception. Set this only if you wish to override, on this call 176 | only, the value set during the geocoder's initialization. 177 | 178 | :param language: Prefer results in specific languages. Accepts 179 | a single string like ``"en"`` or a list like ``["de", "en"]``. 180 | :type language: str or list 181 | 182 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 183 | ``exactly_one=False``. 184 | """ 185 | params = {'key': self.api_key} 186 | 187 | if isinstance(language, str): 188 | language = [language] 189 | if language: 190 | params['language'] = ','.join(language) 191 | 192 | point = self._coerce_point_to_string(query, "%(lon)s,%(lat)s") 193 | quoted_query = quote(point.encode('utf-8')) 194 | url = "?".join((self.api % dict(query=quoted_query), 195 | urlencode(params))) 196 | logger.debug("%s.reverse: %s", self.__class__.__name__, url) 197 | callback = partial(self._parse_json, exactly_one=exactly_one) 198 | return self._call_geocoder(url, callback, timeout=timeout) 199 | -------------------------------------------------------------------------------- /geopy/geocoders/mapquest.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import urlencode 3 | 4 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 5 | from geopy.location import Location 6 | from geopy.util import logger 7 | 8 | __all__ = ("MapQuest", ) 9 | 10 | 11 | class MapQuest(Geocoder): 12 | """Geocoder using the MapQuest API based on Licensed data. 13 | 14 | Documentation at: 15 | https://developer.mapquest.com/documentation/geocoding-api/ 16 | 17 | MapQuest provides two Geocoding APIs: 18 | 19 | - :class:`geopy.geocoders.OpenMapQuest` Nominatim-alike API 20 | which is based on Open data from OpenStreetMap. 21 | - :class:`geopy.geocoders.MapQuest` (this class) MapQuest's own API 22 | which is based on Licensed data. 23 | """ 24 | 25 | geocode_path = '/geocoding/v1/address' 26 | reverse_path = '/geocoding/v1/reverse' 27 | 28 | def __init__( 29 | self, 30 | api_key, 31 | *, 32 | scheme=None, 33 | timeout=DEFAULT_SENTINEL, 34 | proxies=DEFAULT_SENTINEL, 35 | user_agent=None, 36 | ssl_context=DEFAULT_SENTINEL, 37 | adapter_factory=None, 38 | domain='www.mapquestapi.com' 39 | ): 40 | """ 41 | :param str api_key: The API key required by Mapquest to perform 42 | geocoding requests. API keys are managed through MapQuest's "Manage Keys" 43 | page (https://developer.mapquest.com/user/me/apps). 44 | 45 | :param str scheme: 46 | See :attr:`geopy.geocoders.options.default_scheme`. 47 | 48 | :param int timeout: 49 | See :attr:`geopy.geocoders.options.default_timeout`. 50 | 51 | :param dict proxies: 52 | See :attr:`geopy.geocoders.options.default_proxies`. 53 | 54 | :param str user_agent: 55 | See :attr:`geopy.geocoders.options.default_user_agent`. 56 | 57 | :type ssl_context: :class:`ssl.SSLContext` 58 | :param ssl_context: 59 | See :attr:`geopy.geocoders.options.default_ssl_context`. 60 | 61 | :param callable adapter_factory: 62 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 63 | 64 | .. versionadded:: 2.0 65 | 66 | :param str domain: base api domain for mapquest 67 | """ 68 | super().__init__( 69 | scheme=scheme, 70 | timeout=timeout, 71 | proxies=proxies, 72 | user_agent=user_agent, 73 | ssl_context=ssl_context, 74 | adapter_factory=adapter_factory, 75 | ) 76 | 77 | self.api_key = api_key 78 | self.domain = domain.strip('/') 79 | 80 | self.geocode_api = ( 81 | '%s://%s%s' % (self.scheme, self.domain, self.geocode_path) 82 | ) 83 | self.reverse_api = ( 84 | '%s://%s%s' % (self.scheme, self.domain, self.reverse_path) 85 | ) 86 | 87 | def _parse_json(self, json, exactly_one=True): 88 | '''Returns location, (latitude, longitude) from json feed.''' 89 | features = json['results'][0]['locations'] 90 | 91 | if features == []: 92 | return None 93 | 94 | def parse_location(feature): 95 | addr_keys = [ 96 | 'street', 97 | 'adminArea6', 98 | 'adminArea5', 99 | 'adminArea4', 100 | 'adminArea3', 101 | 'adminArea2', 102 | 'adminArea1', 103 | 'postalCode' 104 | ] 105 | 106 | location = [feature[k] for k in addr_keys if feature.get(k)] 107 | return ", ".join(location) 108 | 109 | def parse_feature(feature): 110 | location = parse_location(feature) 111 | longitude = feature['latLng']['lng'] 112 | latitude = feature['latLng']['lat'] 113 | return Location(location, (latitude, longitude), feature) 114 | 115 | if exactly_one: 116 | return parse_feature(features[0]) 117 | else: 118 | return [parse_feature(feature) for feature in features] 119 | 120 | def geocode( 121 | self, 122 | query, 123 | *, 124 | exactly_one=True, 125 | timeout=DEFAULT_SENTINEL, 126 | limit=None, 127 | bounds=None 128 | ): 129 | """ 130 | Return a location point by address. 131 | 132 | :param str query: The address or query you wish to geocode. 133 | 134 | :param bool exactly_one: Return one result or a list of results, if 135 | available. 136 | 137 | :param int timeout: Time, in seconds, to wait for the geocoding service 138 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 139 | exception. Set this only if you wish to override, on this call 140 | only, the value set during the geocoder's initialization. 141 | 142 | :param int limit: Limit the maximum number of items in the 143 | response. This will be reset to one if ``exactly_one`` is True. 144 | 145 | :param bounds: The bounding box of the viewport within which 146 | to bias geocode results more prominently. 147 | Example: ``[Point(22, 180), Point(-22, -180)]``. 148 | :type bounds: list or tuple of 2 items of :class:`geopy.point.Point` or 149 | ``(latitude, longitude)`` or ``"%(latitude)s, %(longitude)s"``. 150 | 151 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 152 | ``exactly_one=False``. 153 | """ 154 | params = {} 155 | params['key'] = self.api_key 156 | params['location'] = query 157 | 158 | if limit is not None: 159 | params['maxResults'] = limit 160 | 161 | if exactly_one: 162 | params["maxResults"] = 1 163 | 164 | if bounds: 165 | params['boundingBox'] = self._format_bounding_box( 166 | bounds, "%(lat2)s,%(lon1)s,%(lat1)s,%(lon2)s" 167 | ) 168 | 169 | url = '?'.join((self.geocode_api, urlencode(params))) 170 | 171 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 172 | callback = partial(self._parse_json, exactly_one=exactly_one) 173 | return self._call_geocoder(url, callback, timeout=timeout) 174 | 175 | def reverse( 176 | self, 177 | query, 178 | *, 179 | exactly_one=True, 180 | timeout=DEFAULT_SENTINEL 181 | ): 182 | """ 183 | Return an address by location point. 184 | 185 | :param query: The coordinates for which you wish to obtain the 186 | closest human-readable addresses. 187 | :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, 188 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 189 | 190 | :param bool exactly_one: Return one result or a list of results, if 191 | available. 192 | 193 | :param int timeout: Time, in seconds, to wait for the geocoding service 194 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 195 | exception. Set this only if you wish to override, on this call 196 | only, the value set during the geocoder's initialization. 197 | 198 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 199 | ``exactly_one=False``. 200 | """ 201 | params = {} 202 | params['key'] = self.api_key 203 | 204 | point = self._coerce_point_to_string(query, "%(lat)s,%(lon)s") 205 | params['location'] = point 206 | 207 | url = '?'.join((self.reverse_api, urlencode(params))) 208 | 209 | logger.debug("%s.reverse: %s", self.__class__.__name__, url) 210 | callback = partial(self._parse_json, exactly_one=exactly_one) 211 | return self._call_geocoder(url, callback, timeout=timeout) 212 | -------------------------------------------------------------------------------- /geopy/geocoders/yandex.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import urlencode 3 | 4 | from geopy.exc import GeocoderParseError, GeocoderServiceError 5 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 6 | from geopy.location import Location 7 | from geopy.util import logger 8 | 9 | __all__ = ("Yandex", ) 10 | 11 | 12 | class Yandex(Geocoder): 13 | """Yandex geocoder. 14 | 15 | Documentation at: 16 | https://tech.yandex.com/maps/doc/geocoder/desc/concepts/input_params-docpage/ 17 | """ 18 | 19 | api_path = '/1.x/' 20 | 21 | def __init__( 22 | self, 23 | api_key, 24 | *, 25 | timeout=DEFAULT_SENTINEL, 26 | proxies=DEFAULT_SENTINEL, 27 | user_agent=None, 28 | scheme=None, 29 | ssl_context=DEFAULT_SENTINEL, 30 | adapter_factory=None 31 | ): 32 | """ 33 | 34 | :param str api_key: Yandex API key, mandatory. 35 | The key can be created at https://developer.tech.yandex.ru/ 36 | 37 | :param int timeout: 38 | See :attr:`geopy.geocoders.options.default_timeout`. 39 | 40 | :param dict proxies: 41 | See :attr:`geopy.geocoders.options.default_proxies`. 42 | 43 | :param str user_agent: 44 | See :attr:`geopy.geocoders.options.default_user_agent`. 45 | 46 | :param str scheme: 47 | See :attr:`geopy.geocoders.options.default_scheme`. 48 | 49 | :type ssl_context: :class:`ssl.SSLContext` 50 | :param ssl_context: 51 | See :attr:`geopy.geocoders.options.default_ssl_context`. 52 | 53 | :param callable adapter_factory: 54 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 55 | 56 | .. versionadded:: 2.0 57 | """ 58 | super().__init__( 59 | scheme=scheme, 60 | timeout=timeout, 61 | proxies=proxies, 62 | user_agent=user_agent, 63 | ssl_context=ssl_context, 64 | adapter_factory=adapter_factory, 65 | ) 66 | self.api_key = api_key 67 | domain = 'geocode-maps.yandex.ru' 68 | self.api = '%s://%s%s' % (self.scheme, domain, self.api_path) 69 | 70 | def geocode( 71 | self, 72 | query, 73 | *, 74 | exactly_one=True, 75 | timeout=DEFAULT_SENTINEL, 76 | lang=None 77 | ): 78 | """ 79 | Return a location point by address. 80 | 81 | :param str query: The address or query you wish to geocode. 82 | 83 | :param bool exactly_one: Return one result or a list of results, if 84 | available. 85 | 86 | :param int timeout: Time, in seconds, to wait for the geocoding service 87 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 88 | exception. Set this only if you wish to override, on this call 89 | only, the value set during the geocoder's initialization. 90 | 91 | :param str lang: Language of the response and regional settings 92 | of the map. List of supported values: 93 | 94 | - ``tr_TR`` -- Turkish (only for maps of Turkey); 95 | - ``en_RU`` -- response in English, Russian map features; 96 | - ``en_US`` -- response in English, American map features; 97 | - ``ru_RU`` -- Russian (default); 98 | - ``uk_UA`` -- Ukrainian; 99 | - ``be_BY`` -- Belarusian. 100 | 101 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 102 | ``exactly_one=False``. 103 | """ 104 | params = { 105 | 'geocode': query, 106 | 'format': 'json' 107 | } 108 | params['apikey'] = self.api_key 109 | if lang: 110 | params['lang'] = lang 111 | if exactly_one: 112 | params['results'] = 1 113 | url = "?".join((self.api, urlencode(params))) 114 | logger.debug("%s.geocode: %s", self.__class__.__name__, url) 115 | callback = partial(self._parse_json, exactly_one=exactly_one) 116 | return self._call_geocoder(url, callback, timeout=timeout) 117 | 118 | def reverse( 119 | self, 120 | query, 121 | *, 122 | exactly_one=True, 123 | timeout=DEFAULT_SENTINEL, 124 | kind=None, 125 | lang=None 126 | ): 127 | """ 128 | Return an address by location point. 129 | 130 | :param query: The coordinates for which you wish to obtain the 131 | closest human-readable addresses. 132 | :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, 133 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 134 | 135 | :param bool exactly_one: Return one result or a list of results, if 136 | available. 137 | 138 | :param int timeout: Time, in seconds, to wait for the geocoding service 139 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 140 | exception. Set this only if you wish to override, on this call 141 | only, the value set during the geocoder's initialization. 142 | 143 | :param str kind: Type of toponym. Allowed values: `house`, `street`, `metro`, 144 | `district`, `locality`. 145 | 146 | :param str lang: Language of the response and regional settings 147 | of the map. List of supported values: 148 | 149 | - ``tr_TR`` -- Turkish (only for maps of Turkey); 150 | - ``en_RU`` -- response in English, Russian map features; 151 | - ``en_US`` -- response in English, American map features; 152 | - ``ru_RU`` -- Russian (default); 153 | - ``uk_UA`` -- Ukrainian; 154 | - ``be_BY`` -- Belarusian. 155 | 156 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 157 | ``exactly_one=False``. 158 | """ 159 | 160 | try: 161 | point = self._coerce_point_to_string(query, "%(lon)s,%(lat)s") 162 | except ValueError: 163 | raise ValueError("Must be a coordinate pair or Point") 164 | params = { 165 | 'geocode': point, 166 | 'format': 'json' 167 | } 168 | params['apikey'] = self.api_key 169 | if lang: 170 | params['lang'] = lang 171 | if kind: 172 | params['kind'] = kind 173 | url = "?".join((self.api, urlencode(params))) 174 | logger.debug("%s.reverse: %s", self.__class__.__name__, url) 175 | callback = partial(self._parse_json, exactly_one=exactly_one) 176 | return self._call_geocoder(url, callback, timeout=timeout) 177 | 178 | def _parse_json(self, doc, exactly_one): 179 | """ 180 | Parse JSON response body. 181 | """ 182 | if doc.get('error'): 183 | raise GeocoderServiceError(doc['error']['message']) 184 | 185 | try: 186 | places = doc['response']['GeoObjectCollection']['featureMember'] 187 | except KeyError: 188 | raise GeocoderParseError('Failed to parse server response') 189 | 190 | def parse_code(place): 191 | """ 192 | Parse each record. 193 | """ 194 | try: 195 | place = place['GeoObject'] 196 | except KeyError: 197 | raise GeocoderParseError('Failed to parse server response') 198 | 199 | longitude, latitude = [ 200 | float(_) for _ in place['Point']['pos'].split(' ') 201 | ] 202 | 203 | name_elements = ['name', 'description'] 204 | location = ', '.join([place[k] for k in name_elements if place.get(k)]) 205 | 206 | return Location(location, (latitude, longitude), place) 207 | 208 | if exactly_one: 209 | try: 210 | return parse_code(places[0]) 211 | except IndexError: 212 | return None 213 | else: 214 | return [parse_code(place) for place in places] 215 | -------------------------------------------------------------------------------- /geopy/geocoders/pelias.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from urllib.parse import urlencode 3 | 4 | from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder 5 | from geopy.location import Location 6 | from geopy.util import logger 7 | 8 | __all__ = ("Pelias", ) 9 | 10 | 11 | class Pelias(Geocoder): 12 | """Pelias geocoder. 13 | 14 | Documentation at: 15 | https://github.com/pelias/documentation 16 | 17 | See also :class:`geopy.geocoders.GeocodeEarth` which is a Pelias-based 18 | service provided by the developers of Pelias itself. 19 | """ 20 | 21 | geocode_path = '/v1/search' 22 | reverse_path = '/v1/reverse' 23 | 24 | def __init__( 25 | self, 26 | domain, 27 | api_key=None, 28 | *, 29 | timeout=DEFAULT_SENTINEL, 30 | proxies=DEFAULT_SENTINEL, 31 | user_agent=None, 32 | scheme=None, 33 | ssl_context=DEFAULT_SENTINEL, 34 | adapter_factory=None 35 | # Make sure to synchronize the changes of this signature in the 36 | # inheriting classes (e.g. GeocodeEarth). 37 | ): 38 | """ 39 | :param str domain: Specify a domain for Pelias API. 40 | 41 | :param str api_key: Pelias API key, optional. 42 | 43 | :param int timeout: 44 | See :attr:`geopy.geocoders.options.default_timeout`. 45 | 46 | :param dict proxies: 47 | See :attr:`geopy.geocoders.options.default_proxies`. 48 | 49 | :param str user_agent: 50 | See :attr:`geopy.geocoders.options.default_user_agent`. 51 | 52 | :param str scheme: 53 | See :attr:`geopy.geocoders.options.default_scheme`. 54 | 55 | :type ssl_context: :class:`ssl.SSLContext` 56 | :param ssl_context: 57 | See :attr:`geopy.geocoders.options.default_ssl_context`. 58 | 59 | :param callable adapter_factory: 60 | See :attr:`geopy.geocoders.options.default_adapter_factory`. 61 | 62 | .. versionadded:: 2.0 63 | 64 | """ 65 | super().__init__( 66 | scheme=scheme, 67 | timeout=timeout, 68 | proxies=proxies, 69 | user_agent=user_agent, 70 | ssl_context=ssl_context, 71 | adapter_factory=adapter_factory, 72 | ) 73 | 74 | self.api_key = api_key 75 | self.domain = domain.strip('/') 76 | 77 | self.geocode_api = ( 78 | '%s://%s%s' % (self.scheme, self.domain, self.geocode_path) 79 | ) 80 | self.reverse_api = ( 81 | '%s://%s%s' % (self.scheme, self.domain, self.reverse_path) 82 | ) 83 | 84 | def geocode( 85 | self, 86 | query, 87 | *, 88 | exactly_one=True, 89 | timeout=DEFAULT_SENTINEL, 90 | boundary_rect=None, 91 | country_bias=None, 92 | language=None 93 | ): 94 | """ 95 | Return a location point by address. 96 | 97 | :param str query: The address or query you wish to geocode. 98 | 99 | :param bool exactly_one: Return one result or a list of results, if 100 | available. 101 | 102 | :param int timeout: Time, in seconds, to wait for the geocoding service 103 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 104 | exception. Set this only if you wish to override, on this call 105 | only, the value set during the geocoder's initialization. 106 | 107 | :type boundary_rect: list or tuple of 2 items of :class:`geopy.point.Point` 108 | or ``(latitude, longitude)`` or ``"%(latitude)s, %(longitude)s"``. 109 | :param boundary_rect: Coordinates to restrict search within. 110 | Example: ``[Point(22, 180), Point(-22, -180)]``. 111 | 112 | :param str country_bias: Bias results to this country (ISO alpha-3). 113 | 114 | :param str language: Preferred language in which to return results. 115 | Either uses standard 116 | `RFC2616 `_ 117 | accept-language string or a simple comma-separated 118 | list of language codes. 119 | 120 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 121 | ``exactly_one=False``. 122 | """ 123 | params = {'text': query} 124 | 125 | if self.api_key: 126 | params.update({ 127 | 'api_key': self.api_key 128 | }) 129 | 130 | if boundary_rect: 131 | lon1, lat1, lon2, lat2 = self._format_bounding_box( 132 | boundary_rect, "%(lon1)s,%(lat1)s,%(lon2)s,%(lat2)s").split(',') 133 | params['boundary.rect.min_lon'] = lon1 134 | params['boundary.rect.min_lat'] = lat1 135 | params['boundary.rect.max_lon'] = lon2 136 | params['boundary.rect.max_lat'] = lat2 137 | 138 | if country_bias: 139 | params['boundary.country'] = country_bias 140 | 141 | if language: 142 | params["lang"] = language 143 | 144 | url = "?".join((self.geocode_api, urlencode(params))) 145 | logger.debug("%s.geocode_api: %s", self.__class__.__name__, url) 146 | callback = partial(self._parse_json, exactly_one=exactly_one) 147 | return self._call_geocoder(url, callback, timeout=timeout) 148 | 149 | def reverse( 150 | self, 151 | query, 152 | *, 153 | exactly_one=True, 154 | timeout=DEFAULT_SENTINEL, 155 | language=None 156 | ): 157 | """ 158 | Return an address by location point. 159 | 160 | :param query: The coordinates for which you wish to obtain the 161 | closest human-readable addresses. 162 | :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, 163 | longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. 164 | 165 | :param bool exactly_one: Return one result or a list of results, if 166 | available. 167 | 168 | :param int timeout: Time, in seconds, to wait for the geocoding service 169 | to respond before raising a :class:`geopy.exc.GeocoderTimedOut` 170 | exception. Set this only if you wish to override, on this call 171 | only, the value set during the geocoder's initialization. 172 | 173 | :param str language: Preferred language in which to return results. 174 | Either uses standard 175 | `RFC2616 `_ 176 | accept-language string or a simple comma-separated 177 | list of language codes. 178 | 179 | :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if 180 | ``exactly_one=False``. 181 | """ 182 | try: 183 | lat, lon = self._coerce_point_to_string(query).split(',') 184 | except ValueError: 185 | raise ValueError("Must be a coordinate pair or Point") 186 | params = { 187 | 'point.lat': lat, 188 | 'point.lon': lon, 189 | } 190 | 191 | if language: 192 | params['lang'] = language 193 | 194 | if self.api_key: 195 | params.update({ 196 | 'api_key': self.api_key 197 | }) 198 | 199 | url = "?".join((self.reverse_api, urlencode(params))) 200 | logger.debug("%s.reverse: %s", self.__class__.__name__, url) 201 | callback = partial(self._parse_json, exactly_one=exactly_one) 202 | return self._call_geocoder(url, callback, timeout=timeout) 203 | 204 | def _parse_code(self, feature): 205 | # Parse each resource. 206 | latitude = feature.get('geometry', {}).get('coordinates', [])[1] 207 | longitude = feature.get('geometry', {}).get('coordinates', [])[0] 208 | placename = feature.get('properties', {}).get('name') 209 | return Location(placename, (latitude, longitude), feature) 210 | 211 | def _parse_json(self, response, exactly_one): 212 | if response is None: 213 | return None 214 | features = response['features'] 215 | if not len(features): 216 | return None 217 | if exactly_one: 218 | return self._parse_code(features[0]) 219 | else: 220 | return [self._parse_code(feature) for feature in features] 221 | --------------------------------------------------------------------------------