diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8e8df..9e8abb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project follows [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- OAuth 2.0 example in README and in the `examples` directory + +### Changed +- Check if a passed `session` is authenticated and use this instead of Username/Password, this enables OAuth 2.0 authentication ## [4.0.0] - 2023-07-15 ### Added diff --git a/Makefile b/Makefile index 03a5dfd..5fec1dc 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ deps: ## Install dependencies python -m pip install --upgrade pip python -m pip install -r requirements.txt python -m pip install -r test-requirements.txt + pre-commit install docs: ## Generate documentation python -m pdoc -o docs osmapi diff --git a/README.md b/README.md index 7e4aa34..125c08e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ To update the online documentation, you need to re-generate the documentation wi To test this library, please create an account on the [development server of OpenStreetMap (https://api06.dev.openstreetmap.org)](https://api06.dev.openstreetmap.org). +Check the [examples directory](https://github.com/metaodi/osmapi/tree/develop/examples) to find more example code. + ### Read from OpenStreetMap ```python @@ -67,22 +69,81 @@ Note: Each line in the password file should have the format _user:password_ >>> api.ChangesetClose() ``` -## Note +### OAuth authentication + +Username/Password authentication will be deprecated in 2024 (see [official OWG announcemnt](https://www.openstreetmap.org/user/pnorman/diary/401157) for details). +In order to use this library in the future, you'll need to use OAuth 2.0. + +To use OAuth 2.0, you must register an application with an OpenStreetMap account, either on the [development server](https://master.apis.dev.openstreetmap.org/oauth2/applications) or on the [production server](https://www.openstreetmap.org/oauth2/applications). +Once this registration is done, you'll get a `client_id` and a `client_secret` that you can use to authenticate users. + +Example code using [`requests-oauth2client`](https://pypi.org/project/requests-oauth2client/): + +```python +from requests_oauth2client import OAuth2Client, OAuth2AuthorizationCodeAuth +import requests +import webbrowser +import osmapi +import os + +client_id = "" +client_secret = "" + +# special value for redirect_uri for non-web applications +redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + +authorization_base_url = "https://master.apis.dev.openstreetmap.org/oauth2/authorize" +token_url = "https://master.apis.dev.openstreetmap.org/oauth2/token" + +oauth2client = OAuth2Client( + token_endpoint=token_url, + authorization_endpoint=authorization_base_url, + redirect_uri=redirect_uri, + client_id=client_id, + client_secret=client_secret, + code_challenge_method=None, +) + +# open OSM website to authrorize user using the write_api and write_notes scope +scope = ["write_api", "write_notes"] +az_request = oauth2client.authorization_request(scope=scope) +print(f"Authorize user using this URL: {az_request.uri}") +webbrowser.open(az_request.uri) + +# create a new requests session using the OAuth authorization +auth_code = input("Paste the authorization code here: ") +auth = OAuth2AuthorizationCodeAuth( + oauth2client, + auth_code, + redirect_uri=redirect_uri, +) +oauth_session = requests.Session() +oauth_session.auth = auth + +# use the custom session +api = osmapi.OsmApi( + api="https://api06.dev.openstreetmap.org", + session=oauth_session +) +with api.Changeset({"comment": "My first test"}) as changeset_id: + print(f"Part of Changeset {changeset_id}") + node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) + print(node1) +``` + +## Note about imports / automated edits Scripted imports and automated edits should only be carried out by those with experience and understanding of the way the OpenStreetMap community creates maps, and only with careful **planning** and **consultation** with the local community. See the [Import/Guidelines](http://wiki.openstreetmap.org/wiki/Import/Guidelines) and [Automated Edits/Code of Conduct](http://wiki.openstreetmap.org/wiki/Automated_Edits/Code_of_Conduct) for more information. -### Development +## Development If you want to help with the development of `osmapi`, you should clone this repository and install the requirements: - pip install -r requirements.txt - pip install -r test-requirements.txt + make deps -After that, it is recommended to install the pre-commit-hooks (flake8, black): - - pre-commit install +Better yet use the provided [`setup.sh`](https://github.com/metaodi/osmapi/blob/develop/setup.sh) script to create a virtual env and install this package in it. You can lint the source code using this command: @@ -92,8 +153,6 @@ And if you want to reformat the files (using the black code style) simply run: make format -### Tests - To run the tests use the following command: make test diff --git a/examples/oauth2.py b/examples/oauth2.py new file mode 100644 index 0000000..ffa8dfe --- /dev/null +++ b/examples/oauth2.py @@ -0,0 +1,52 @@ +# install oauthlib for requests: pip install requests-oauth2client +from requests_oauth2client import OAuth2Client, OAuth2AuthorizationCodeAuth +import requests +import webbrowser +import osmapi +from dotenv import load_dotenv, find_dotenv +import os + +load_dotenv(find_dotenv()) + +# Credentials you get from registering a new application +# register here: https://master.apis.dev.openstreetmap.org/oauth2/applications +# or on production: https://www.openstreetmap.org/oauth2/applications +client_id = os.getenv("OSM_OAUTH_CLIENT_ID") +client_secret = os.getenv("OSM_OAUTH_CLIENT_SECRET") + +# special value for redirect_uri for non-web applications +redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + +authorization_base_url = "https://master.apis.dev.openstreetmap.org/oauth2/authorize" +token_url = "https://master.apis.dev.openstreetmap.org/oauth2/token" + +oauth2client = OAuth2Client( + token_endpoint=token_url, + authorization_endpoint=authorization_base_url, + redirect_uri=redirect_uri, + auth=(client_id, client_secret), + code_challenge_method=None, +) + +# open OSM website to authrorize user using the write_api and write_notes scope +scope = ["write_api", "write_notes"] +az_request = oauth2client.authorization_request(scope=scope) +print(f"Authorize user using this URL: {az_request.uri}") +webbrowser.open(az_request.uri) + +# create a new requests session using the OAuth authorization +auth_code = input("Paste the authorization code here: ") +auth = OAuth2AuthorizationCodeAuth( + oauth2client, + auth_code, + redirect_uri=redirect_uri, +) +oauth_session = requests.Session() +oauth_session.auth = auth + +# use the custom session +api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", session=oauth_session) +with api.Changeset({"comment": "My first test"}) as changeset_id: + print(f"Part of Changeset {changeset_id}") + node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) + print(node1) diff --git a/osmapi/http.py b/osmapi/http.py index 068fe6b..1bd54ac 100644 --- a/osmapi/http.py +++ b/osmapi/http.py @@ -17,7 +17,13 @@ class OsmApiSession: def __init__(self, base_url, created_by, auth=None, session=None): self._api = base_url self._created_by = created_by - self._auth = auth + + try: + self._auth = auth + if not auth and session.auth: + self._auth = session.auth + except AttributeError: + pass self._http_session = session self._session = self._get_http_session() diff --git a/setup.sh b/setup.sh index 6adc0a1..ab1f28f 100755 --- a/setup.sh +++ b/setup.sh @@ -3,7 +3,5 @@ [ ! -d pyenv ] && python -m venv pyenv source pyenv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt -pip install -r test-requirements.txt +make deps pip install -e . diff --git a/tests/fixtures/test_NodeCreate_with_session_auth.xml b/tests/fixtures/test_NodeCreate_with_session_auth.xml new file mode 100644 index 0000000..e11a615 --- /dev/null +++ b/tests/fixtures/test_NodeCreate_with_session_auth.xml @@ -0,0 +1 @@ +3322 diff --git a/tests/fixtures/test_NodeCreate_wo_auth.xml b/tests/fixtures/test_NodeCreate_wo_auth.xml new file mode 100644 index 0000000..190a180 --- /dev/null +++ b/tests/fixtures/test_NodeCreate_wo_auth.xml @@ -0,0 +1 @@ +123 diff --git a/tests/node_test.py b/tests/node_test.py index 059bd78..9e081ab 100644 --- a/tests/node_test.py +++ b/tests/node_test.py @@ -2,6 +2,7 @@ import osmapi from unittest import mock import datetime +from requests.auth import HTTPBasicAuth class TestOsmApiNode(osmapi_test.TestOsmApi): @@ -158,6 +159,26 @@ def test_NodeCreate_wo_auth(self): ): self.api.NodeCreate(test_node) + def test_NodeCreate_with_session_auth(self): + self._session_mock() + self.session_mock.auth = HTTPBasicAuth("user", "pass") + + api = osmapi.OsmApi(api=self.api_base, session=self.session_mock) + + # setup mock + api.ChangesetCreate = mock.Mock(return_value=1111) + api._CurrentChangesetId = 1111 + test_node = { + "lat": 47.287, + "lon": 8.765, + "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, + } + + cs = api.ChangesetCreate({"comment": "This is a test dataset"}) + self.assertEqual(cs, 1111) + result = api.NodeCreate(test_node) + self.assertEqual(result["id"], 3322) + def test_NodeCreate_with_exception(self): self._session_mock(auth=True) self.api._session._http_request = mock.Mock(side_effect=Exception) diff --git a/tests/osmapi_test.py b/tests/osmapi_test.py index cc4a7a0..4441970 100644 --- a/tests/osmapi_test.py +++ b/tests/osmapi_test.py @@ -26,6 +26,7 @@ def _session_mock(self, auth=False, filenames=None, status=200): self.session_mock = mock.Mock() self.session_mock.request = mock.Mock(return_value=response_mock) + self.session_mock.auth = None if auth: self.api = OsmApi(