diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a51f019 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 4 diff --git a/.flake8 b/.flake8 index ac7b48a..b6e5688 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,4 @@ [flake8] +exclude = .env inline-quotes = " - +max-line-length = 88 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2399809..2c59e51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,10 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.6, 3.7] + python-version: [3.6, 3.7, 3.8] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 diff --git a/.isort.cfg b/.isort.cfg index 22a03c4..cc8fddb 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,5 +1,6 @@ [settings] -line-length=79 -multi_line_output=3 combine_as_imports=true include_trailing_comma=true +line-length=88 +multi_line_output=3 +use_parentheses = true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4381a1b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -dist: xenial -sudo: false - -language: python -cache: pip - -python: - - "3.6" - - "3.7" - -install: - - python setup.py develop - - pip install coveralls -r requirements.txt - -script: - - flake8 - - black --check ./ - - coverage run --source yandex_geocoder -m pytest - -after_success: - - coveralls - -deploy: - provider: pypi - user: $PyPiLogin - password: $PyPiPassword - on: - tags: true - python: "3.7" diff --git a/README.md b/README.md index a146eff..9ae4633 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Yandex Geocoder === Get address coordinates via Yandex geocoder -[![Build Status](https://github.com/sivakov512/yandex-geocoder/workflows/test/badge.svg)](https://github.com/sivakov512/yandex-geocoder) +[![Build Status](https://github.com/sivakov512/yandex-geocoder/workflows/test/badge.svg)](https://github.com/sivakov512/yandex-geocoder/actions?query=workflow%3Atest) [![Coverage Status](https://coveralls.io/repos/github/sivakov512/yandex-geocoder/badge.svg?branch=master)](https://coveralls.io/github/sivakov512/yandex-geocoder?branch=master) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![Python versions](https://img.shields.io/pypi/pyversions/yandex-geocoder.svg)](https://pypi.python.org/pypi/yandex-geocoder) @@ -18,10 +18,21 @@ pip install yandex-geocoder Usage example --- +Yandex Geocoder requires an API developer key, you can get it [here](https://developer.tech.yandex.ru/services/) to use this library. ``` python +from decimal import Decimal + from yandex_geocoder import Client -Client.coordinates('Хабаровск 60 октября 150') # ('135.114326', '48.47839') + + +client = Client("your-api-key") + +coordinates = client.coordinates("Москва Льва Толстого 16") +assert coordinates == (Decimal("37.587093"), Decimal("55.733969")) + +address = client.address(Decimal("37.587093"), Decimal("55.733969")) +assert address == "Россия, Москва, улица Льва Толстого, 16" ``` Development and contribution @@ -49,7 +60,3 @@ black --check ./ ``` * feel free to contribute! - -Credits ---- -- [f213](https://github.com/f213) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index f3497b3..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[tool.black] -line-length = 79 - diff --git a/requirements.txt b/requirements.txt index 25fda34..49e0d05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,36 +1,36 @@ appdirs==1.4.3 -atomicwrites==1.3.0 attrs==19.3.0 -black==19.3b0 -certifi==2019.9.11 +black==19.10b0 +certifi==2019.11.28 chardet==3.0.4 Click==7.0 -coverage==4.5.4 +coverage==5.0.3 entrypoints==0.3 -flake8==3.7.8 -flake8-debugger==3.2.0 -flake8-isort==2.7.0 -flake8-print==3.1.1 -flake8-quotes==2.1.0 +flake8==3.7.9 +flake8-debugger==3.2.1 +flake8-isort==2.8.0 +flake8-print==3.1.4 +flake8-quotes==2.1.1 idna==2.8 -importlib-metadata==0.23 isort==4.3.21 mccabe==0.6.1 -more-itertools==7.2.0 -packaging==19.2 -pluggy==0.13.0 -py==1.8.0 +more-itertools==8.2.0 +packaging==20.1 +pathspec==0.7.0 +pluggy==0.13.1 +py==1.8.1 pycodestyle==2.5.0 pyflakes==2.1.1 -pyparsing==2.4.2 -pytest==5.2.2 +pyparsing==2.4.6 +pytest==5.3.5 pytest-cov==2.8.1 -pytest-mock==1.11.2 +pytest-mock==2.0.0 +regex==2020.1.8 requests==2.22.0 requests-mock==1.7.0 -six==1.12.0 -testfixtures==6.10.0 +six==1.14.0 +testfixtures==6.12.0 toml==0.10.0 -urllib3==1.25.6 -wcwidth==0.1.7 -zipp==0.6.0 +typed-ast==1.4.1 +urllib3==1.25.8 +wcwidth==0.1.8 diff --git a/setup.py b/setup.py index d83b22a..76a3f0d 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,9 @@ def read(fname): setup( author="Nikita Sivakov", author_email="sivakov512@gmail.com", - description=( - "Simple library for getting address coordinates via Yandex geocoder" - ), + description="Simple library for getting address or coordinates via Yandex geocoder", install_requires=["requests~=2.22"], - keywords="yandex geocoder geo coordinates maps api", + keywords="yandex geocoder geo coordinates address maps api", license="MIT", long_description=read("README.md"), long_description_content_type="text/markdown", @@ -23,10 +21,11 @@ def read(fname): packages=["yandex_geocoder"], python_requires=">=3.6", url="https://github.com/sivakov512/yandex-geocoder", - version="1.0.1", + version="2.0.0", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..466b3d6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +import json +from urllib.parse import urlencode + +import pytest +import requests_mock + + +@pytest.fixture +def mock_api(): + def _encode(geocode: str, api_key: str = "well-known-key") -> str: + params = {"format": "json", "apikey": api_key, "geocode": geocode} + query = urlencode(params) + return f"https://geocode-maps.yandex.ru/1.x/?{query}" + + with requests_mock.mock() as _m: + yield lambda resp, status, **encode_kw: _m.get( + _encode(**encode_kw), + json=load_fixture(resp) if isinstance(resp, str) else resp, + status_code=status, + ) + + +def load_fixture(fixture_name: str) -> dict: + with open(f"./tests/fixtures/{fixture_name}.json") as fixture: + return json.load(fixture) + + +@pytest.fixture +def mock_client_response(mocker): + def _mock(fixture_name): + with open("./tests/fixtures/{}.json".format(fixture_name)) as fixture: + return mocker.patch( + "yandex_geocoder.Client.request", return_value=json.load(fixture), + ) + + return _mock diff --git a/tests/fixtures/address_found.json b/tests/fixtures/address_found.json new file mode 100644 index 0000000..480e6ac --- /dev/null +++ b/tests/fixtures/address_found.json @@ -0,0 +1,502 @@ +{ + "response": { + "GeoObjectCollection": { + "metaDataProperty": { + "GeocoderResponseMetaData": { + "Point": { "pos": "37.587093 55.733969" }, + "request": "37.587093,55.733969", + "results": "10", + "found": "9" + } + }, + "featureMember": [ + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "exact", + "text": "Россия, Москва, улица Льва Толстого, 16", + "kind": "house", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва, улица Льва Толстого, 16", + "postal_code": "119021", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { + "kind": "locality", + "name": "Москва" + }, + { + "kind": "street", + "name": "улица Льва Толстого" + }, + { "kind": "house", "name": "16" } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва, улица Льва Толстого, 16", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва", + "Thoroughfare": { + "ThoroughfareName": "улица Льва Толстого", + "Premise": { + "PremiseNumber": "16", + "PostalCode": { + "PostalCodeNumber": "119021" + } + } + } + } + } + } + } + } + }, + "name": "улица Льва Толстого, 16", + "description": "Москва, Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.582987 55.731653", + "upperCorner": "37.591198 55.736285" + } + }, + "Point": { "pos": "37.587093 55.733969" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "street", + "text": "Россия, Москва, улица Льва Толстого", + "kind": "street", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва, улица Льва Толстого", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { + "kind": "locality", + "name": "Москва" + }, + { + "kind": "street", + "name": "улица Льва Толстого" + } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва, улица Льва Толстого", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва", + "Thoroughfare": { + "ThoroughfareName": "улица Льва Толстого" + } + } + } + } + } + } + }, + "name": "улица Льва Толстого", + "description": "Москва, Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.582475 55.731556", + "upperCorner": "37.59118 55.736868" + } + }, + "Point": { "pos": "37.58667 55.733984" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия, Москва, Центральный административный округ, район Хамовники, квартал Красная Роза", + "kind": "district", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва, Центральный административный округ, район Хамовники, квартал Красная Роза", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { + "kind": "locality", + "name": "Москва" + }, + { + "kind": "district", + "name": "Центральный административный округ" + }, + { + "kind": "district", + "name": "район Хамовники" + }, + { + "kind": "district", + "name": "квартал Красная Роза" + } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва, Центральный административный округ, район Хамовники, квартал Красная Роза", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва", + "DependentLocality": { + "DependentLocalityName": "Центральный административный округ", + "DependentLocality": { + "DependentLocalityName": "район Хамовники", + "DependentLocality": { + "DependentLocalityName": "квартал Красная Роза" + } + } + } + } + } + } + } + } + }, + "name": "квартал Красная Роза", + "description": "район Хамовники, Центральный административный округ, Москва, Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.5834 55.731536", + "upperCorner": "37.59233 55.737025" + } + }, + "Point": { "pos": "37.587721 55.734233" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия, Москва, Центральный административный округ, район Хамовники", + "kind": "district", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва, Центральный административный округ, район Хамовники", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { + "kind": "locality", + "name": "Москва" + }, + { + "kind": "district", + "name": "Центральный административный округ" + }, + { + "kind": "district", + "name": "район Хамовники" + } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва, Центральный административный округ, район Хамовники", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва", + "DependentLocality": { + "DependentLocalityName": "Центральный административный округ", + "DependentLocality": { + "DependentLocalityName": "район Хамовники" + } + } + } + } + } + } + } + }, + "name": "район Хамовники", + "description": "Центральный административный округ, Москва, Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.540928 55.710276", + "upperCorner": "37.612236 55.750383" + } + }, + "Point": { "pos": "37.574525 55.729199" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия, Москва, Центральный административный округ", + "kind": "district", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва, Центральный административный округ", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { + "kind": "locality", + "name": "Москва" + }, + { + "kind": "district", + "name": "Центральный административный округ" + } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва, Центральный административный округ", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва", + "DependentLocality": { + "DependentLocalityName": "Центральный административный округ" + } + } + } + } + } + } + }, + "name": "Центральный административный округ", + "description": "Москва, Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.51441 55.710276", + "upperCorner": "37.713593 55.797108" + } + }, + "Point": { "pos": "37.614069 55.753995" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия, Москва", + "kind": "locality", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { "kind": "locality", "name": "Москва" } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва" + } + } + } + } + } + }, + "name": "Москва", + "description": "Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.326051 55.49133", + "upperCorner": "37.96779 55.957565" + } + }, + "Point": { "pos": "37.617635 55.755814" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия, Москва", + "kind": "province", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { "kind": "province", "name": "Москва" } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва" + } + } + } + } + }, + "name": "Москва", + "description": "Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "36.803259 55.142221", + "upperCorner": "37.96779 56.021281" + } + }, + "Point": { "pos": "37.622504 55.753215" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия, Центральный федеральный округ", + "kind": "province", + "Address": { + "country_code": "RU", + "formatted": "Россия, Центральный федеральный округ", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Центральный федеральный округ", + "CountryNameCode": "RU", + "CountryName": "Россия" + } + } + } + }, + "name": "Центральный федеральный округ", + "description": "Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "30.750266 49.556986", + "upperCorner": "47.641729 59.625172" + } + }, + "Point": { "pos": "38.064718 54.873745" } + } + }, + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "other", + "text": "Россия", + "kind": "country", + "Address": { + "country_code": "RU", + "formatted": "Россия", + "Components": [ + { "kind": "country", "name": "Россия" } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия", + "CountryNameCode": "RU", + "CountryName": "Россия" + } + } + } + }, + "name": "Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "19.484764 41.18599", + "upperCorner": "191.128003 81.886117" + } + }, + "Point": { "pos": "99.505405 61.698653" } + } + } + ] + } + } +} diff --git a/tests/fixtures/address_not_found.json b/tests/fixtures/address_not_found.json new file mode 100644 index 0000000..743f6b8 --- /dev/null +++ b/tests/fixtures/address_not_found.json @@ -0,0 +1,15 @@ +{ + "response": { + "GeoObjectCollection": { + "metaDataProperty": { + "GeocoderResponseMetaData": { + "Point": { "pos": "-22.412907 55.733969" }, + "request": "337.587093,55.733969", + "results": "10", + "found": "0" + } + }, + "featureMember": [] + } + } +} diff --git a/tests/fixtures/coords_found.json b/tests/fixtures/coords_found.json index 574eba8..0bbe0f2 100644 --- a/tests/fixtures/coords_found.json +++ b/tests/fixtures/coords_found.json @@ -1,66 +1,62 @@ { - "GeoObjectCollection": { - "metaDataProperty": { - "GeocoderResponseMetaData": { - "request": "Москва, улица Новый Арбат, дом 24", - "found": "1", - "results": "10" - } - }, - "featureMember": [ - { - "GeoObject": { - "metaDataProperty": { - "GeocoderMetaData": { - "kind": "house", - "text": "Россия, Москва, улица Новый Арбат, 24", - "precision": "exact", - "Address": { - "country_code": "RU", - "postal_code": "119019", - "formatted": "Москва, улица Новый Арбат, 24", - "Components": [ - { - "kind": "country", - "name": "Россия" - }, - { - "kind": "province", - "name": "Центральный федеральный округ" - }, - { - "kind": "province", - "name": "Москва" - }, - { - "kind": "locality", - "name": "Москва" - }, - { - "kind": "street", - "name": "улица Новый Арбат" - }, - { - "kind": "house", - "name": "24" - } - ] - }, - "AddressDetails": { - "Country": { - "AddressLine": "Москва, улица Новый Арбат, 24", - "CountryNameCode": "RU", - "CountryName": "Россия", - "AdministrativeArea": { - "AdministrativeAreaName": "Москва", - "Locality": { - "LocalityName": "Москва", - "Thoroughfare": { - "ThoroughfareName": "улица Новый Арбат", - "Premise": { - "PremiseNumber": "24", - "PostalCode": { - "PostalCodeNumber": "119019" + "response": { + "GeoObjectCollection": { + "metaDataProperty": { + "GeocoderResponseMetaData": { + "request": "Москва Льва Толстого 16", + "results": "10", + "found": "1" + } + }, + "featureMember": [ + { + "GeoObject": { + "metaDataProperty": { + "GeocoderMetaData": { + "precision": "exact", + "text": "Россия, Москва, улица Льва Толстого, 16", + "kind": "house", + "Address": { + "country_code": "RU", + "formatted": "Россия, Москва, улица Льва Толстого, 16", + "postal_code": "119021", + "Components": [ + { "kind": "country", "name": "Россия" }, + { + "kind": "province", + "name": "Центральный федеральный округ" + }, + { + "kind": "province", + "name": "Москва" + }, + { + "kind": "locality", + "name": "Москва" + }, + { + "kind": "street", + "name": "улица Льва Толстого" + }, + { "kind": "house", "name": "16" } + ] + }, + "AddressDetails": { + "Country": { + "AddressLine": "Россия, Москва, улица Льва Толстого, 16", + "CountryNameCode": "RU", + "CountryName": "Россия", + "AdministrativeArea": { + "AdministrativeAreaName": "Москва", + "Locality": { + "LocalityName": "Москва", + "Thoroughfare": { + "ThoroughfareName": "улица Льва Толстого", + "Premise": { + "PremiseNumber": "16", + "PostalCode": { + "PostalCodeNumber": "119021" + } } } } @@ -68,21 +64,19 @@ } } } - } - }, - "description": "Москва, Россия", - "name": "улица Новый Арбат, 24", - "boundedBy": { - "Envelope": { - "lowerCorner": "37.583508 55.750768", - "upperCorner": "37.591719 55.755398" - } - }, - "Point": { - "pos": "37.587614 55.753083" + }, + "name": "улица Льва Толстого, 16", + "description": "Москва, Россия", + "boundedBy": { + "Envelope": { + "lowerCorner": "37.582987 55.731653", + "upperCorner": "37.591198 55.736285" + } + }, + "Point": { "pos": "37.587093 55.733969" } } } - } - ] + ] + } } } diff --git a/tests/fixtures/coords_not_found.json b/tests/fixtures/coords_not_found.json index aa1aab2..53a8a45 100644 --- a/tests/fixtures/coords_not_found.json +++ b/tests/fixtures/coords_not_found.json @@ -1,12 +1,14 @@ { - "GeoObjectCollection": { - "metaDataProperty": { - "GeocoderResponseMetaData": { - "request": "OhjisuTho5ee", - "found": "0", - "results": "10" - } - }, - "featureMember": [] + "response": { + "GeoObjectCollection": { + "metaDataProperty": { + "GeocoderResponseMetaData": { + "request": "абырвалг", + "results": "10", + "found": "0" + } + }, + "featureMember": [] + } } } diff --git a/tests/test_address.py b/tests/test_address.py new file mode 100644 index 0000000..a278f16 --- /dev/null +++ b/tests/test_address.py @@ -0,0 +1,51 @@ +from decimal import Decimal + +import pytest + +from yandex_geocoder import ( + Client, + InvalidKey, + NothingFound, + UnexpectedResponse, +) + + +def test_returns_found_address(mock_api): + mock_api("address_found", 200, geocode="37.587093,55.733969") + client = Client("well-known-key") + + assert ( + client.address(Decimal("37.587093"), Decimal("55.733969")) + == "Россия, Москва, улица Льва Толстого, 16" + ) + + +def test_raises_if_address_not_found(mock_api): + mock_api("address_not_found", 200, geocode="337.587093,55.733969") + client = Client("well-known-key") + + with pytest.raises(NothingFound, match='Nothing found for "337.587093 55.733969"'): + client.address(Decimal("337.587093"), Decimal("55.733969")) + + +def test_raises_for_invalid_api_key(mock_api): + mock_api( + {"statusCode": 403, "error": "Forbidden", "message": "Invalid key"}, + 403, + geocode="37.587093,55.733969", + api_key="unkown-api-key", + ) + client = Client("unkown-api-key") + + with pytest.raises(InvalidKey): + client.address(Decimal("37.587093"), Decimal("55.733969")) + + +def test_raises_for_unknown_response(mock_api): + mock_api({}, 500, geocode="37.587093,55.733969") + client = Client("well-known-key") + + with pytest.raises(UnexpectedResponse) as exc_info: + client.address(Decimal("37.587093"), Decimal("55.733969")) + + assert "status_code=500, body=b'{}'" in exc_info.value.args diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py index f418eca..ee314bd 100644 --- a/tests/test_coordinates.py +++ b/tests/test_coordinates.py @@ -1,33 +1,51 @@ -import json +from decimal import Decimal import pytest -from yandex_geocoder import Client -from yandex_geocoder.exceptions import YandexGeocoderAddressNotFound +from yandex_geocoder import ( + Client, + InvalidKey, + NothingFound, + UnexpectedResponse, +) -@pytest.fixture -def mock_client_response(mocker): - def _mock(fixture_name): - with open("./tests/fixtures/{}.json".format(fixture_name)) as fixture: - return mocker.patch( - "yandex_geocoder.Client.request", - return_value=json.load(fixture), - ) +def test_returns_found_coordinates(mock_api): + mock_api("coords_found", 200, geocode="Москва Льва Толстого 16") + client = Client("well-known-key") - return _mock + assert client.coordinates("Москва Льва Толстого 16") == ( + Decimal("37.587093"), + Decimal("55.733969"), + ) -def test_returns_found_coordinates(mock_client_response): - mock_client_response("coords_found") +def test_raises_if_coordinates_not_found(mock_api): + mock_api("coords_not_found", 200, geocode="абырвалг") + client = Client("well-known-key") - assert Client.coordinates("some address") == ("37.587614", "55.753083") + with pytest.raises(NothingFound, match='Nothing found for "абырвалг"'): + client.coordinates("абырвалг") -def test_raises_if_coordinates_not_found(mock_client_response): - mock_client_response("coords_not_found") +def test_raises_for_invalid_api_key(mock_api): + mock_api( + {"statusCode": 403, "error": "Forbidden", "message": "Invalid key"}, + 403, + geocode="Москва Льва Толстого 16", + api_key="unkown-api-key", + ) + client = Client("unkown-api-key") - with pytest.raises( - YandexGeocoderAddressNotFound, match='"some address" not found' - ): - Client.coordinates("some address") + with pytest.raises(InvalidKey): + client.coordinates("Москва Льва Толстого 16") + + +def test_raises_for_unknown_response(mock_api): + mock_api({}, 500, geocode="Москва Льва Толстого 16") + client = Client("well-known-key") + + with pytest.raises(UnexpectedResponse) as exc_info: + client.coordinates("Москва Льва Толстого 16") + + assert "status_code=500, body=b'{}'" in exc_info.value.args diff --git a/tests/test_requests.py b/tests/test_requests.py deleted file mode 100644 index 149960d..0000000 --- a/tests/test_requests.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -import requests_mock - -from yandex_geocoder import Client -from yandex_geocoder.exceptions import YandexGeocoderHttpException - - -@pytest.fixture -def mock_response(): - with requests_mock.mock() as _m: - yield lambda **kwargs: _m.get( - "https://geocode-maps.yandex.ru/1.x/?geocode=b&format=json", - **kwargs - ) - - -def test_request_ok(mock_response): - mock_response(json={"response": {"ok": True}}) - - assert Client.request("b") == {"ok": True} - - -def test_request_fails(mock_response): - mock_response(status_code=400) - - with pytest.raises( - YandexGeocoderHttpException, - match="Non-200 response from yandex geocoder", - ): - Client.request("b") diff --git a/yandex_geocoder/__init__.py b/yandex_geocoder/__init__.py index 6c07126..c9933c6 100644 --- a/yandex_geocoder/__init__.py +++ b/yandex_geocoder/__init__.py @@ -1,3 +1,15 @@ from yandex_geocoder.client import Client +from yandex_geocoder.exceptions import ( + InvalidKey, + NothingFound, + UnexpectedResponse, + YandexGeocoderException, +) -__all__ = ["Client"] +__all__ = [ + "Client", + "InvalidKey", + "NothingFound", + "UnexpectedResponse", + "YandexGeocoderException", +] diff --git a/yandex_geocoder/client.py b/yandex_geocoder/client.py index 066d785..6f50334 100644 --- a/yandex_geocoder/client.py +++ b/yandex_geocoder/client.py @@ -1,11 +1,9 @@ -import typing +from decimal import Decimal +from typing import Tuple import requests -from yandex_geocoder.exceptions import ( - YandexGeocoderAddressNotFound, - YandexGeocoderHttpException, -) +from .exceptions import InvalidKey, NothingFound, UnexpectedResponse class Client: @@ -13,47 +11,56 @@ class Client: :Example: >>> from yandex_geocoder import Client - >>> Client.coordinates('Хабаровск 60 октября 150') - ('135.114326', '48.47839') + >>> client = Client("your-api-key") + + >>> coordinates = client.coordinates("Москва Льва Толстого 16") + >>> assert coordinates == (Decimal("37.587093"), Decimal("55.733969")) + + >>> address = client.address(Decimal("37.587093"), Decimal("55.733969")) + >>> assert address == "Россия, Москва, улица Льва Толстого, 16" """ - API_URL = "https://geocode-maps.yandex.ru/1.x/" - PARAMS = {"format": "json"} + __slots__ = ("api_key",) - @classmethod - def request(cls, address: str) -> dict: - """Requests passed address and returns content of `response` key. + api_key: str - Raises `YandexGeocoderHttpException` if response's status code is - different from `200`. + def __init__(self, api_key: str): + self.api_key = api_key - """ + def _request(self, address: str) -> dict: response = requests.get( - cls.API_URL, params=dict(geocode=address, **cls.PARAMS) + "https://geocode-maps.yandex.ru/1.x/", + params=dict(format="json", apikey=self.api_key, geocode=address), ) - if response.status_code != 200: - raise YandexGeocoderHttpException( - "Non-200 response from yandex geocoder" + if response.status_code == 200: + return response.json()["response"] + elif response.status_code == 403: + raise InvalidKey() + else: + raise UnexpectedResponse( + f"status_code={response.status_code}, body={response.content}" ) - return response.json()["response"] + def coordinates(self, address: str) -> Tuple[Decimal]: + """Fetch coordinates (longitude, latitude) for passed address.""" + data = self._request(address)["GeoObjectCollection"]["featureMember"] - @classmethod - def coordinates(cls, address: str) -> typing.Tuple[str, str]: - """Returns a tuple of ccordinates (longtitude, latitude) for - passed address. + if not data: + raise NothingFound(f'Nothing found for "{address}" not found') - Raises `YandexGeocoderAddressNotFound` if nothing found. + coordinates = data[0]["GeoObject"]["Point"]["pos"] # type: str + longitude, latitude = tuple(coordinates.split(" ")) - """ - data = cls.request(address)["GeoObjectCollection"]["featureMember"] + return Decimal(longitude), Decimal(latitude) + + def address(self, longitude: Decimal, latitude: Decimal) -> str: + """Fetch address for passed coordinates.""" + got = self._request(f"{longitude},{latitude}") + data = got["GeoObjectCollection"]["featureMember"] if not data: - raise YandexGeocoderAddressNotFound( - '"{}" not found'.format(address) - ) + raise NothingFound(f'Nothing found for "{longitude} {latitude}"') - coordinates = data[0]["GeoObject"]["Point"]["pos"] # type: str - return tuple(coordinates.split(" ")) + return data[0]["GeoObject"]["metaDataProperty"]["GeocoderMetaData"]["text"] diff --git a/yandex_geocoder/exceptions.py b/yandex_geocoder/exceptions.py index 45e130e..8a50ff2 100644 --- a/yandex_geocoder/exceptions.py +++ b/yandex_geocoder/exceptions.py @@ -2,9 +2,13 @@ class YandexGeocoderException(Exception): pass -class YandexGeocoderHttpException(YandexGeocoderException): +class UnexpectedResponse(YandexGeocoderException): pass -class YandexGeocoderAddressNotFound(YandexGeocoderException): +class NothingFound(YandexGeocoderException): + pass + + +class InvalidKey(YandexGeocoderException): pass