├── .coveragerc ├── .github └── workflows │ ├── build.yml │ └── publish_python.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CNAME ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── index.html ├── osmapi.html ├── osmapi │ ├── OsmApi.html │ ├── dom.html │ ├── errors.html │ ├── http.html │ ├── parser.html │ └── xmlbuilder.html └── search.js ├── examples ├── changesets.py ├── error_handling.py ├── log_output.py ├── notes.py ├── oauth2.py ├── oauth2_backend.py ├── timeout.py └── write_to_osm.py ├── osmapi ├── OsmApi.py ├── __init__.py ├── dom.py ├── errors.py ├── http.py ├── parser.py └── xmlbuilder.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── setup.sh ├── test-requirements.txt ├── test.sh └── tests ├── __init__.py ├── capabilities_test.py ├── changeset_test.py ├── conftest.py ├── dom_test.py ├── fixtures ├── passwordfile.txt ├── passwordfile_colon.txt ├── test_Capabilities.xml ├── test_ChangesetClose.xml ├── test_ChangesetComment.xml ├── test_ChangesetCreate.xml ├── test_ChangesetCreate_with_created_by.xml ├── test_ChangesetCreate_with_open_changeset.xml ├── test_ChangesetDownload.xml ├── test_ChangesetDownloadContainingUnicode.xml ├── test_ChangesetDownload_invalid_response.xml ├── test_ChangesetGet.xml ├── test_ChangesetGetWithComment.xml ├── test_ChangesetGetWithoutDiscussion.xml ├── test_ChangesetSubscribe.xml ├── test_ChangesetSubscribeWhenAlreadySubscribed.xml ├── test_ChangesetUnsubscribe.xml ├── test_ChangesetUnsubscribeWhenNotSubscribed.xml ├── test_ChangesetUpdate.xml ├── test_ChangesetUpdate_with_created_by.xml ├── test_ChangesetUpdate_wo_changeset.xml ├── test_ChangesetUpload_create_node.xml ├── test_ChangesetUpload_delete_relation.xml ├── test_ChangesetUpload_invalid_response.xml ├── test_ChangesetUpload_modify_way.xml ├── test_Changeset_create.xml ├── test_Changeset_create_node.xml ├── test_Changeset_upload.xml ├── test_ChangesetsGet.xml ├── test_NodeCreate.xml ├── test_NodeCreate_changesetauto.xml ├── test_NodeCreate_with_session_auth.xml ├── test_NodeCreate_wo_auth.xml ├── test_NodeDelete.xml ├── test_NodeGet.xml ├── test_NodeGet_invalid_response.xml ├── test_NodeGet_with_version.xml ├── test_NodeHistory.xml ├── test_NodeRelations.xml ├── test_NodeRelationsUnusedElement.xml ├── test_NodeUpdate.xml ├── test_NodeUpdateConflict.xml ├── test_NodeUpdateWhenChangesetIsClosed.xml ├── test_NodeWays.xml ├── test_NodeWaysNotExists.xml ├── test_NodesGet.xml ├── test_NoteAlreadyClosed.xml ├── test_NoteClose.xml ├── test_NoteComment.xml ├── test_NoteCommentAnonymous.xml ├── test_NoteCommentOnClosedNote.xml ├── test_NoteCreate.xml ├── test_NoteCreateAnonymous.xml ├── test_NoteCreate_wo_auth.xml ├── test_NoteGet.xml ├── test_NoteGet_invalid_xml.xml ├── test_NoteReopen.xml ├── test_NotesGet.xml ├── test_NotesGet_empty.xml ├── test_NotesSearch.xml ├── test_RelationCreate.xml ├── test_RelationDelete.xml ├── test_RelationFull.xml ├── test_RelationGet.xml ├── test_RelationGet_with_version.xml ├── test_RelationHistory.xml ├── test_RelationRelations.xml ├── test_RelationRelationsUnusedElement.xml ├── test_RelationUpdate.xml ├── test_RelationsGet.xml ├── test_WayCreate.xml ├── test_WayDelete.xml ├── test_WayFull.xml ├── test_WayFull_invalid_response.xml ├── test_WayGet.xml ├── test_WayGet_nodata.xml ├── test_WayGet_with_version.xml ├── test_WayHistory.xml ├── test_WayRelations.xml ├── test_WayRelationsUnusedElement.xml ├── test_WayUpdate.xml ├── test_WayUpdatePreconditionFailed.xml └── test_WaysGet.xml ├── helper_test.py ├── node_test.py ├── notes_test.py ├── osmapi_test.py ├── relation_test.py └── way_test.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | tests/* 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Test osmapi package 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main, develop] 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10", "3.11"] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | sudo apt-get install pandoc 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install -r test-requirements.txt 29 | pip install -e . 30 | 31 | - name: Test the package 32 | run: ./test.sh 33 | -------------------------------------------------------------------------------- /.github/workflows/publish_python.yml: -------------------------------------------------------------------------------- 1 | # workflow inspired by chezou/tabula-py 2 | name: Upload Python Package 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | strategy: 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11"] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: | 29 | sudo apt-get install pandoc 30 | python -m pip install --upgrade pip 31 | pip install -r requirements.txt 32 | pip install -r test-requirements.txt 33 | pip install -e . 34 | 35 | - name: Test the package 36 | run: ./test.sh 37 | 38 | deploy: 39 | runs-on: ubuntu-latest 40 | needs: [test] 41 | environment: release 42 | permissions: 43 | id-token: write 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Set up Python 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: '3.8' 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install setuptools wheel build 54 | - name: Build 55 | run: | 56 | python -m build 57 | - name: Publish 58 | uses: pypa/gh-action-pypi-publish@release/v1 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | MANIFEST 3 | *.pyc 4 | *.egg-info 5 | .coverage 6 | .tox 7 | .pycache/* 8 | .pytest_cache/* 9 | .env 10 | pyenv 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.7.0 4 | hooks: 5 | - id: black 6 | language_version: python3.8 7 | - repo: https://github.com/pycqa/flake8 8 | rev: 6.0.0 9 | hooks: 10 | - id: flake8 11 | 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project follows [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ## [4.3.0] - 2025-01-21 8 | ### Added 9 | - New `ConnectionApiError` when a connection or network error occurs (see issue #176, thanks [Mateusz Konieczny](https://github.com/matkoniecz)) 10 | - 11 | ### Changed 12 | - Use the pattern `raise XYError from e` to explicitly add the original exceptions as the cause for a new (wrapped) exception. 13 | 14 | ### Removed 15 | - Remove u string prefix (see PR #180, thanks [Boris Verkhovskiy](https://github.com/verhovsky)) 16 | 17 | ## [4.2.0] - 2024-08-08 18 | ### Added 19 | - Add a new `timeout` parameter to `OsmApi` which allows to set a timeout in seconds (default is 30s) for the API requests (see issue #170, thanks [Mateusz Konieczny](https://github.com/matkoniecz)) 20 | 21 | ### Changed 22 | - Only include `discussion` key in result of `ChangesetGet` if `include_discussion=True` (see issue #163, thanks [Mateusz Konieczny](https://github.com/matkoniecz)) 23 | - Update OAuth example in README using [cli-oauth2](https://github.com/Zverik/cli-oauth2) (see PR #169, thanks [Ilya Zverev](https://github.com/Zverik)) 24 | 25 | ## [4.1.0] - 2024-03-19 26 | ### Added 27 | - OAuth 2.0 example in README and in the `examples` directory 28 | 29 | ### Changed 30 | - Check if a passed `session` is authenticated and use this instead of Username/Password, this enables OAuth 2.0 authentication 31 | 32 | ### Removed 33 | - remove Python2 crumbs (see PR #159, thanks [Alexandre Detiste](https://github.com/a-detiste)) 34 | 35 | ## [4.0.0] - 2023-07-15 36 | ### Added 37 | - Add Python 3.11 to build 38 | - Add pre-commit configuration for `flake8` and `black` 39 | 40 | ### Changed 41 | - Upgrade the code with `pyupgrade` (see PR #146, thanks [Miroslav Šedivý](https://github.com/eumiro)) 42 | Miroslav Šedivý 43 | - Replace format with f-strings to resolve issue (see PR #147, thanks [Miroslav Šedivý](https://github.com/eumiro)) 44 | - Use the `black` code style for this code base 45 | 46 | ### Removed 47 | - **BC-Break**: Remove support for Python 3.7, new minimum version for osmapi is Python 3.8 48 | 49 | ## [3.1.0] - 2023-01-18 50 | ### Added 51 | - New `ElementNotFoundApiError` when a 404 response comes from the API 52 | - Raise an exception if a user tries to create a test changeset on the PROD server (see issue #66, thanks [SomeoneElseOSM](https://github.com/SomeoneElseOSM)) 53 | 54 | ### Changed 55 | - Add new `NoteAlreadyClosedApiError` exception when you try to close an already closed note (see issue #135, thanks [Mateusz Konieczny](https://github.com/matkoniecz)) 56 | 57 | ### Fixed 58 | - `NoteGets` now allows empty results i.e. it returns an empty list if no notes were found (see issue #137, thanks [Mateusz Konieczny](https://github.com/matkoniecz)) 59 | 60 | ## [3.0.0] - 2022-02-12 61 | ### Added 62 | - Add context manager `Changeset()` to open/close changesets 63 | - Add `session` parameter to provide a custom http session object 64 | 65 | ### Changed 66 | - Refactor code into several modules/files to improve maintainability 67 | - Use `logging` module to log debug information 68 | 69 | ### Removed 70 | - **BC-Break**: Remove `debug` parameter of OsmApi, replaced debug messages with `logging` module 71 | 72 | ### Fixed 73 | - Added `python_requires` to setup.py to define Python 3.7 as minimum version 74 | 75 | ## [2.0.2] - 2021-11-24 76 | ### Changed 77 | - Set `long_description` format to markdown 78 | 79 | ## [2.0.1] - 2021-11-24 80 | ### Added 81 | - Add Makefile for all common tasks 82 | 83 | ### Fixed 84 | - Long description of osmapi (now using directly the README.md) 85 | 86 | ### Changed 87 | - Switch from nose to pytest 88 | - Move docs to its own subdirectory 89 | 90 | ### Removed 91 | - Remove tox configuration and dependency 92 | 93 | ## [2.0.0] - 2021-11-22 94 | ### Added 95 | - Move from Travis CI to Github Actions 96 | - Add more API-specific errors to catch specific errors (see issue #115, thanks [Mateusz Konieczny](https://github.com/matkoniecz)): 97 | - `ChangesetClosedApiError` 98 | - `NoteClosedApiError` 99 | - `VersionMismatchApiError` 100 | - `PreconditionFailedApiError` 101 | 102 | ### Changed 103 | - **BC-Break**: osmapi does **not** support Python 2.7, 3.3, 3.4, 3.5 and 3.6 anymore 104 | 105 | ### Fixed 106 | - Return an empty list in `NodeRelations`, `WayRelations`, `RelationRelations` and `NodeWays` if the returned XML is empty (thanks [FisherTsai](https://github.com/FisherTsai), see issue #117) 107 | 108 | ## [1.3.0] - 2020-10-05 109 | ### Added 110 | - Add close() method to close the underlying http session (see issue #107) 111 | - Add context manager to automatically open and close the http session (see issue #107) 112 | 113 | ### Fixed 114 | - Correctly parse password file (thanks [Julien Palard](https://github.com/JulienPalard), see pull request #106) 115 | 116 | ## [1.2.2] - 2018-11-05 117 | ### Fixed 118 | - Update PyPI password for deployment 119 | 120 | ## [1.2.1] - 2018-11-05 121 | ### Fixed 122 | - Deployment to PyPI with Travis 123 | 124 | ## [1.2.0] - 2018-11-05 125 | ### Added 126 | - Support Python 3.7 (thanks a lot [cclauss](https://github.com/cclauss)) 127 | 128 | ### Removed 129 | - Python 3.3 is no longer supported (EOL) 130 | 131 | ### Changed 132 | - Updated dependencies for Python 3.7 133 | - Adapt README to use Python 3 syntax (thanks [cclauss](https://github.com/cclauss)) 134 | 135 | ## [1.1.0] - 2017-10-11 136 | ### Added 137 | - Raise new `XmlResponseInvalidError` if XML response from the OpenStreetMap API is invalid 138 | 139 | ### Changed 140 | - Improved README (thanks [Mateusz Konieczny](https://github.com/matkoniecz)) 141 | 142 | ## [1.0.2] - 2017-09-07 143 | ### Added 144 | - Rais ResponseEmptyApiError if we expect a response from the OpenStreetMap API, but didn't get one 145 | 146 | ### Removed 147 | - Removed httpretty as HTTP mock library 148 | 149 | ## [1.0.1] - 2017-09-07 150 | ### Fixed 151 | - Make sure tests run offline 152 | 153 | ## [1.0.0] - 2017-09-05 154 | ### Added 155 | - Officially support Python 3.5 and 3.6 156 | 157 | ### Removed 158 | - osmapi does **not** support Python 2.6 anymore (it might work, it might not) 159 | 160 | ### Changed 161 | - **BC-Break:** raise an exception if the requested element is deleted (previoulsy `None` has been returned) 162 | 163 | ## [0.8.1] - 2016-12-21 164 | ### Fixed 165 | - Use setuptools instead of distutils in setup.py 166 | 167 | ## [0.8.0] - 2016-12-21 168 | ### Removed 169 | - This release no longer supports Python 3.2, if you need it, go back to release <= 0.6.2 170 | 171 | ## Changed 172 | - Read version from __init__.py instead of importing it in setup.py 173 | 174 | ## [0.7.2] - 2016-12-21 175 | ### Fixed 176 | - Added 'requests' as a dependency to setup.py to fix installation problems 177 | 178 | ## [0.7.1] - 2016-12-12 179 | ### Changed 180 | - Catch OSError in setup.py to avoid installation errors 181 | 182 | ## [0.7.0] - 2016-12-07 183 | ### Changed 184 | - Replace the old httplib with requests library (thanks a lot [Austin Hartzheim](http://austinhartzheim.me/)!) 185 | - Use format strings instead of ugly string concatenation 186 | - Fix unicode in changesets (thanks a lot to [MichaelVL](https://github.com/MichaelVL)!) 187 | 188 | ## [0.6.2] - 2016-01-04 189 | ### Changed 190 | - Re-arranged README 191 | - Make sure PyPI releases are only created when a release has been tagged on GitHub 192 | 193 | ## [0.6.1] - 2016-01-04 194 | ### Changed 195 | - The documentation is now available at a new domain: http://osmapi.metaodi.ch, the previous provider does no longer provide this service 196 | 197 | ## [0.6.0] - 2015-05-26 198 | ### Added 199 | - SSL support for the API calls (thanks [Austin Hartzheim](http://austinhartzheim.me/)!) 200 | - Run tests on Python 3.4 as well 201 | - A bunch of new *Error classes (see below) 202 | - Dependency to 'Pygments' to enable syntax highlighting for [online documentation](http://osmapi.divshot.io) 203 | - [Contributing guidelines](https://github.com/metaodi/osmapi/blob/master/CONTRIBUTING.md) 204 | 205 | ### Changed 206 | - Changed generic `Exception` with more specific ones, so a client can catch those and react accordingly (no BC-break!) 207 | 208 | ## [0.5.0] - 2015-01-03 209 | ### Changed 210 | - BC-break: all dates are now parsed as datetime objects 211 | 212 | ### Added 213 | - Implementation for changeset discussions (ChangesetComment, ChangesetSubscribe, ChangesetUnsubscribe) 214 | - When (un)subscribing to a changeset, there are two special errors `AlreadySubscribedApiError` and `NotSubscribedApiError` to check for 215 | - The ChangesetGet method got a new parameter `include_discussion` to determine wheter or not changeset discussion should be in the response 216 | 217 | ## [0.4.2] - 2015-01-01 218 | ### Fixed 219 | - Result of `NodeWay` is now actually parsed as a `way` 220 | 221 | ### Added 222 | - Lots of method comments for documentation 223 | 224 | ### Changed 225 | - Update to pdoc 0.3.1 which changed the appearance of the online docs 226 | 227 | ## [0.4.1] - 2014-10-08 228 | ### Changed 229 | - Parse dates in notes as `datetime` objects 230 | 231 | ## [0.4.0] - 2014-10-07 232 | ### Added 233 | - Release for OSM Notes API 234 | - Generation of online documentation (http://osmapi.divshot.io) 235 | 236 | ## [0.3.1] - 2014-06-21 237 | ### Fixed 238 | - Hotfix release of Python 3.x (base64) 239 | 240 | ## [0.3.0] - 2014-05-20 241 | ### Added 242 | - Support for Python 3.x 243 | - Use `tox` to run tests against multiple versions of Python 244 | 245 | ## [0.2.26] - 2014-05-02 246 | ### Fixed 247 | - Fixed notes again 248 | 249 | ## [0.2.25] - 2014-05-02 250 | ### Fixed 251 | - Unit tests for basic functionality 252 | - Fixed based on the unit tests (previously undetected bugs) 253 | 254 | ## [0.2.24] - 2014-01-07 255 | ### Fixed 256 | - Fixed notes 257 | 258 | ## [0.2.23] - 2014-01-03 259 | ### Changed 260 | - Hotfix release 261 | 262 | ## [0.2.22] - 2014-01-03 263 | ### Fixed 264 | - Fixed README.md not found error during installation 265 | 266 | ## [0.2.21] - 2014-01-03 267 | ### Changed 268 | - Updated description 269 | 270 | ## [0.2.20] - 2014-01-01 271 | ### Added 272 | - First release of PyPI package "osmapi" 273 | 274 | ## [0.2.19] - 2014-01-01 275 | ### Changed 276 | - Inital version from SVN (http://svn.openstreetmap.org/applications/utils/python_lib/OsmApi/OsmApi.py) 277 | - Move to GitHub 278 | 279 | ## 0.2.19 - 2010-05-24 280 | ### Changed 281 | - Add debug message on ApiError 282 | 283 | ## 0.2.18 - 2010-04-20 284 | ### Fixed 285 | - Fix ChangesetClose and _http_request 286 | 287 | ## 0.2.17 - 2010-01-02 288 | ### Added 289 | - Capabilities implementation 290 | 291 | ## 0.2.16 - 2010-01-02 292 | ### Changed 293 | - ChangesetsGet by Alexander Rampp 294 | 295 | ## 0.2.15 - 2009-12-16 296 | ### Fixed 297 | - xml encoding error for < and > 298 | 299 | ## 0.2.14 - 2009-11-20 300 | ### Changed 301 | - changesetautomulti parameter 302 | 303 | ## 0.2.13 - 2009-11-16 304 | ### Changed 305 | - modify instead update for osc 306 | 307 | ## 0.2.12 - 2009-11-14 308 | ### Added 309 | - raise ApiError on 4xx errors 310 | 311 | ## 0.2.11 - 2009-10-14 312 | ### Fixed 313 | - unicode error on ChangesetUpload 314 | 315 | ## 0.2.10 - 2009-10-14 316 | ### Added 317 | - RelationFullRecur definition 318 | 319 | ## 0.2.9 - 2009-10-13 320 | ### Added 321 | - automatic changeset management 322 | - ChangesetUpload implementation 323 | 324 | ## 0.2.8 - 2009-10-13 325 | ### Changed 326 | - *(Create|Update|Delete) use not unique _do method 327 | 328 | ## 0.2.7 - 2009-10-09 329 | ### Added 330 | - implement all missing functions except ChangesetsGet and GetCapabilities 331 | 332 | ## 0.2.6 - 2009-10-09 333 | ### Changed 334 | - encoding clean-up 335 | 336 | ## 0.2.5 - 2009-10-09 337 | ### Added 338 | - implements NodesGet, WaysGet, RelationsGet, ParseOsm, ParseOsc 339 | 340 | ## 0.2.4 - 2009-10-06 clean-up 341 | ### Changed 342 | - clean-up 343 | 344 | ## 0.2.3 - 2009-09-09 345 | ### Changed 346 | - keep http connection alive for multiple request 347 | - (Node|Way|Relation)Get return None when object have been deleted (raising error before) 348 | 349 | ## 0.2.2 - 2009-07-13 350 | ### Added 351 | - can identify applications built on top of the lib 352 | 353 | ## 0.2.1 - 2009-05-05 354 | ### Changed 355 | - some changes in constructor 356 | 357 | ## 0.2 - 2009-05-01 358 | ### Added 359 | - initial import 360 | 361 | 362 | # Categories 363 | - `Added` for new features. 364 | - `Changed` for changes in existing functionality. 365 | - `Deprecated` for once-stable features removed in upcoming releases. 366 | - `Removed` for deprecated features removed in this release. 367 | - `Fixed` for any bug fixes. 368 | - `Security` to invite users to upgrade in case of vulnerabilities. 369 | 370 | [Unreleased]: https://github.com/metaodi/osmapi/compare/v4.3.0...HEAD 371 | [4.3.0]: https://github.com/metaodi/osmapi/compare/v4.2.0...v4.3.0 372 | [4.2.0]: https://github.com/metaodi/osmapi/compare/v4.1.0...v4.2.0 373 | [4.1.0]: https://github.com/metaodi/osmapi/compare/v4.0.0...v4.1.0 374 | [4.0.0]: https://github.com/metaodi/osmapi/compare/v3.1.0...v4.0.0 375 | [3.1.0]: https://github.com/metaodi/osmapi/compare/v3.0.0...v3.1.0 376 | [3.0.0]: https://github.com/metaodi/osmapi/compare/v2.0.2...v3.0.0 377 | [2.0.2]: https://github.com/metaodi/osmapi/compare/v2.0.1...v2.0.2 378 | [2.0.1]: https://github.com/metaodi/osmapi/compare/v2.0.0...v2.0.1 379 | [2.0.0]: https://github.com/metaodi/osmapi/compare/v1.3.0...v2.0.0 380 | [1.3.0]: https://github.com/metaodi/osmapi/compare/v1.2.2...v1.3.0 381 | [1.2.2]: https://github.com/metaodi/osmapi/compare/v1.2.1...v1.2.2 382 | [1.2.1]: https://github.com/metaodi/osmapi/compare/v1.2.0...v1.2.1 383 | [1.2.0]: https://github.com/metaodi/osmapi/compare/v1.1.0...v1.2.0 384 | [1.1.0]: https://github.com/metaodi/osmapi/compare/v1.0.2...v1.1.0 385 | [1.0.2]: https://github.com/metaodi/osmapi/compare/v1.0.1...v1.0.2 386 | [1.0.1]: https://github.com/metaodi/osmapi/compare/v1.0.0...v1.0.1 387 | [1.0.0]: https://github.com/metaodi/osmapi/compare/v0.8.1...v1.0.0 388 | [0.8.1]: https://github.com/metaodi/osmapi/compare/v0.8.0...v0.8.1 389 | [0.8.0]: https://github.com/metaodi/osmapi/compare/v0.7.2...v0.8.0 390 | [0.7.2]: https://github.com/metaodi/osmapi/compare/v0.7.1...v0.7.2 391 | [0.7.1]: https://github.com/metaodi/osmapi/compare/v0.7.0...v0.7.1 392 | [0.7.0]: https://github.com/metaodi/osmapi/compare/v0.6.2...v0.7.0 393 | [0.6.2]: https://github.com/metaodi/osmapi/compare/v0.6.1...v0.6.2 394 | [0.6.1]: https://github.com/metaodi/osmapi/compare/v0.6.0...v0.6.1 395 | [0.6.0]: https://github.com/metaodi/osmapi/compare/v0.5.0...v0.6.0 396 | [0.5.0]: https://github.com/metaodi/osmapi/compare/v0.4.2...v0.5.0 397 | [0.4.2]: https://github.com/metaodi/osmapi/compare/v0.4.1...v0.4.2 398 | [0.4.1]: https://github.com/metaodi/osmapi/compare/v0.4.0...v0.4.1 399 | [0.4.0]: https://github.com/metaodi/osmapi/compare/v0.3.1...v0.4.0 400 | [0.3.1]: https://github.com/metaodi/osmapi/compare/v0.3.0...v0.3.1 401 | [0.3.0]: https://github.com/metaodi/osmapi/compare/v0.2.26...v0.3.0 402 | [0.2.26]: https://github.com/metaodi/osmapi/compare/v0.2.25...v0.2.26 403 | [0.2.25]: https://github.com/metaodi/osmapi/compare/v0.2.24...v0.2.25 404 | [0.2.24]: https://github.com/metaodi/osmapi/compare/v0.2.23...v0.2.24 405 | [0.2.23]: https://github.com/metaodi/osmapi/compare/v0.2.22...v0.2.23 406 | [0.2.22]: https://github.com/metaodi/osmapi/compare/v0.2.21...v0.2.22 407 | [0.2.21]: https://github.com/metaodi/osmapi/compare/v0.2.20...v0.2.21 408 | [0.2.20]: https://github.com/metaodi/osmapi/compare/v0.2.19...v0.2.20 409 | [0.2.19]: https://github.com/metaodi/osmapi/releases/tag/v0.2.19 410 | 411 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | osmapi.metaodi.ch 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you want to participate in this project, please follow this guidline. 4 | 5 | Fork and clone this repository: 6 | 7 | ```bash 8 | git clone git@github.com:your-username/osmapi.git 9 | ``` 10 | 11 | Install the dependencies using `pip`: 12 | 13 | ```bash 14 | pip install -r requirements.txt 15 | pip install -r test-requirements.txt 16 | ``` 17 | 18 | Make sure the tests pass: 19 | 20 | ```bash 21 | nosetests --verbose 22 | ``` 23 | 24 | You can even run the tests on different versions of Python with `tox`: 25 | 26 | ```bash 27 | tox 28 | ``` 29 | 30 | To ensure a good quality of the code use `flake8` to check the code style: 31 | 32 | ```bash 33 | flake8 --install-hook 34 | ``` 35 | 36 | ## Create a pull request 37 | 38 | 1. Choose the `develop` branch as a target for new/changed functionality, `master` should only be targeted for urgent bugfixes. 39 | 2. While it's not strictly required, it's highly recommended to create a new branch on your fork for each pull request. 40 | 3. Push to your fork and [submit a pull request][pr]. 41 | 4. Check if the [build ran successfully][ci] and try to improve your code if not. 42 | 43 | At this point you're waiting for my review. 44 | I might suggest some changes or improvements or alternatives. 45 | 46 | Some things that will increase the chance that your pull request is accepted: 47 | 48 | * Write tests. 49 | * Follow the Python style guide ([PEP-8][pep8]). 50 | * Write a [good commit message][commit]. 51 | 52 | [pr]: https://github.com/metaodi/osmapi/compare/ 53 | [ci]: https://travis-ci.org/metaodi/osmapi 54 | [pep8]: https://www.python.org/dev/peps/pep-0008/ 55 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 56 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | .PHONY: coverage deps help lint test docs 3 | 4 | coverage: ## Run tests with coverage 5 | python -m coverage erase 6 | python -m coverage run --include=osmapi/* -m pytest -ra 7 | python -m coverage report -m 8 | 9 | deps: ## Install dependencies 10 | python -m pip install --upgrade pip 11 | python -m pip install -r requirements.txt 12 | python -m pip install -r test-requirements.txt 13 | pre-commit install 14 | 15 | docs: ## Generate documentation 16 | python -m pdoc -o docs osmapi 17 | 18 | format: ## Format source code (black codestyle) 19 | python -m black osmapi examples tests *.py 20 | 21 | lint: ## Linting of source code 22 | python -m black --check --diff osmapi examples tests *.py 23 | python -m flake8 --statistics --show-source . 24 | 25 | test: ## Run tests (run in UTF-8 mode in Windows) 26 | python -Xutf8 -m pytest --cov=osmapi tests/ 27 | 28 | help: SHELL := /bin/bash 29 | help: ## Show help message 30 | @IFS=$$'\n' ; \ 31 | help_lines=(`fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##/:/'`); \ 32 | printf "%s\n\n" "Usage: make [task]"; \ 33 | printf "%-20s %s\n" "task" "help" ; \ 34 | printf "%-20s %s\n" "------" "----" ; \ 35 | for help_line in $${help_lines[@]}; do \ 36 | IFS=$$':' ; \ 37 | help_split=($$help_line) ; \ 38 | help_command=`echo $${help_split[0]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ 39 | help_info=`echo $${help_split[2]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ 40 | printf '\033[36m'; \ 41 | printf "%-20s %s" $$help_command ; \ 42 | printf '\033[0m'; \ 43 | printf "%s\n" $$help_info; \ 44 | done 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | osmapi 2 | ====== 3 | 4 | [![Build osmapi](https://github.com/metaodi/osmapi/actions/workflows/build.yml/badge.svg)](https://github.com/metaodi/osmapi/actions/workflows/build.yml) 5 | [![Version](https://img.shields.io/pypi/v/osmapi.svg)](https://pypi.python.org/pypi/osmapi/) 6 | [![License](https://img.shields.io/pypi/l/osmapi.svg)](https://github.com/metaodi/osmapi/blob/develop/LICENSE.txt) 7 | [![Coverage](https://img.shields.io/coveralls/metaodi/osmapi/develop.svg)](https://coveralls.io/r/metaodi/osmapi?branch=develop) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) 10 | 11 | 12 | Python wrapper for the OSM API (requires Python >= 3.8) 13 | 14 | ## Installation 15 | 16 | Install [`osmapi` from PyPi](https://pypi.python.org/pypi/osmapi) by using pip: 17 | 18 | pip install osmapi 19 | 20 | ## Documentation 21 | 22 | The documentation is generated using `pdoc` and can be [viewed online](http://osmapi.metaodi.ch). 23 | 24 | The build the documentation locally, you can use 25 | 26 | make docs 27 | 28 | This project uses GitHub Pages to publish its documentation. 29 | To update the online documentation, you need to re-generate the documentation with the above command and update the `main` branch of this repository. 30 | 31 | ## Examples 32 | 33 | To test this library, please create an account on the [development server of OpenStreetMap (https://api06.dev.openstreetmap.org)](https://api06.dev.openstreetmap.org). 34 | 35 | Check the [examples directory](https://github.com/metaodi/osmapi/tree/develop/examples) to find more example code. 36 | 37 | ### Read from OpenStreetMap 38 | 39 | ```python 40 | >>> import osmapi 41 | >>> api = osmapi.OsmApi() 42 | >>> print(api.NodeGet(123)) 43 | {'changeset': 532907, 'uid': 14298, 44 | 'timestamp': '2007-09-29T09:19:17Z', 45 | 'lon': 10.790009299999999, 'visible': True, 46 | 'version': 1, 'user': 'Mede', 47 | 'lat': 59.9503044, 'tag': {}, 'id': 123} 48 | ``` 49 | 50 | ### Write to OpenStreetMap 51 | 52 | ```python 53 | >>> import osmapi 54 | >>> api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", username = "metaodi", password = "*******") 55 | >>> api.ChangesetCreate({"comment": "My first test"}) 56 | >>> print(api.NodeCreate({"lon":1, "lat":1, "tag": {}})) 57 | {'changeset': 532907, 'lon': 1, 'version': 1, 'lat': 1, 'tag': {}, 'id': 164684} 58 | >>> api.ChangesetClose() 59 | ``` 60 | 61 | ### OAuth authentication 62 | 63 | Username/Password authentication will be deprecated in July 2024 64 | (see [official OWG announcemnt](https://blog.openstreetmap.org/2024/04/17/oauth-1-0a-and-http-basic-auth-shutdown-on-openstreetmap-org/) for details). 65 | In order to use this library in the future, you'll need to use OAuth 2.0. 66 | 67 | To use OAuth 2.0, you must register an application with an OpenStreetMap account, either on the 68 | [development server](https://master.apis.dev.openstreetmap.org/oauth2/applications) 69 | or on the [production server](https://www.openstreetmap.org/oauth2/applications). 70 | Once this registration is done, you'll get a `client_id` and a `client_secret` that you can use to authenticate users. 71 | 72 | Example code using [`cli-oauth2`](https://github.com/Zverik/cli-oauth2) on the development server, replace `OpenStreetMapDevAuth` with `OpenStreetMapAuth` to use the production server: 73 | 74 | ```python 75 | import osmapi 76 | from oauthcli import OpenStreetMapDevAuth 77 | 78 | client_id = "" 79 | client_secret = "" 80 | 81 | auth = OpenStreetMapDevAuth( 82 | client_id, client_secret, ['read_prefs', 'write_map'] 83 | ).auth_code() 84 | 85 | api = osmapi.OsmApi( 86 | api="https://api06.dev.openstreetmap.org", 87 | session=auth.session 88 | ) 89 | 90 | with api.Changeset({"comment": "My first test"}) as changeset_id: 91 | print(f"Part of Changeset {changeset_id}") 92 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 93 | print(node1) 94 | ``` 95 | 96 | An alternative way using the `requests-oauthlib` library can be found 97 | [in the examples](https://github.com/metaodi/osmapi/blob/develop/examples/oauth2.py). 98 | 99 | 100 | ### User agent / credit for application 101 | 102 | To credit the application that supplies changes to OSM, an `appid` can be provided. 103 | This is a string identifying the application. 104 | If this is omitted "osmapi" is used. 105 | 106 | ```python 107 | api = osmapi.OsmApi( 108 | api="https://api06.dev.openstreetmap.org", 109 | appid="MyOSM Script" 110 | ) 111 | ``` 112 | 113 | If then changesets are made using this osmapi instance, they get a tag `created_by` with the following content: `MyOSM Script (osmapi/)` 114 | 115 | [Example changeset of `Kort` using osmapi](https://www.openstreetmap.org/changeset/55197785) 116 | 117 | ## Note about imports / automated edits 118 | 119 | 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. 120 | 121 | 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. 122 | 123 | ## Development 124 | 125 | If you want to help with the development of `osmapi`, you should clone this repository and install the requirements: 126 | 127 | make deps 128 | 129 | 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. 130 | 131 | You can lint the source code using this command: 132 | 133 | make lint 134 | 135 | And if you want to reformat the files (using the black code style) simply run: 136 | 137 | make format 138 | 139 | To run the tests use the following command: 140 | 141 | make test 142 | 143 | ## Release 144 | 145 | To create a new release, follow these steps (please respect [Semantic Versioning](http://semver.org/)): 146 | 147 | 1. Adapt the version number in `osmapi/__init__.py` 148 | 1. Update the CHANGELOG with the version 149 | 1. Re-build the documentation (`make docs`) 150 | 1. Create a [pull request to merge develop into main](https://github.com/metaodi/osmapi/compare/main...develop) (make sure the tests pass!) 151 | 1. Create a [new release/tag on GitHub](https://github.com/metaodi/osmapi/releases) (on the main branch) 152 | 1. The [publication on PyPI](https://pypi.python.org/pypi/osmapi) happens via [GitHub Actions](https://github.com/metaodi/osmapi/actions/workflows/publish_python.yml) on every tagged commit 153 | 154 | ## Attribution 155 | 156 | This project was orginally developed by Etienne Chové. 157 | This repository is a copy of the original code from SVN (http://svn.openstreetmap.org/applications/utils/python_lib/OsmApi/OsmApi.py), with the goal to enable easy contribution via GitHub and release of this package via [PyPI](https://pypi.python.org/pypi/osmapi). 158 | 159 | See also the OSM wiki: http://wiki.openstreetmap.org/wiki/Osmapi 160 | -------------------------------------------------------------------------------- /examples/changesets.py: -------------------------------------------------------------------------------- 1 | import osmapi 2 | from pprint import pprint 3 | 4 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") 5 | 6 | try: 7 | api.ChangesetGet(111111111111) 8 | except osmapi.ApiError as e: 9 | print(f"Error: {e}") 10 | if e.status == 404: 11 | print("Changeset not found") 12 | 13 | print("") 14 | pprint(api.ChangesetGet(12345)) 15 | -------------------------------------------------------------------------------- /examples/error_handling.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv, find_dotenv 2 | from oauthcli import OpenStreetMapDevAuth 3 | from oauthlib.oauth2.rfc6749.errors import OAuth2Error 4 | import logging 5 | import os 6 | import osmapi 7 | import requests 8 | import subprocess 9 | import sys 10 | import urllib3 11 | 12 | 13 | load_dotenv(find_dotenv()) 14 | 15 | # logging setup 16 | log = logging.getLogger(__name__) 17 | loglevel = logging.DEBUG 18 | logging.basicConfig( 19 | format="%(asctime)s %(name)s %(levelname)-8s %(message)s", 20 | level=loglevel, 21 | datefmt="%Y-%m-%d %H:%M:%S", 22 | ) 23 | logging.captureWarnings(True) 24 | 25 | # shut up DEBUG messages of specific loggers 26 | logging.getLogger(osmapi.dom.__name__).setLevel(logging.INFO) 27 | logging.getLogger(urllib3.__name__).setLevel(logging.INFO) 28 | 29 | 30 | def clear_screen(): 31 | # check and make call for specific operating system 32 | _ = subprocess.call("clear" if os.name == "posix" else "cls") 33 | 34 | 35 | # The error handling with osmapi is very easy, simply catch the 36 | # exception for the specific case you want to handle. 37 | # - All osmapi excepctions are child classes of osmapi.OsmApiError 38 | # - Errors that result from the communication with the OSM server osmapi.ApiError 39 | # - There are a number of subclasses to differantiate the different errors 40 | # - catch more specific errors first, then use more general error classes 41 | 42 | # Upload data to OSM without a changeset 43 | log.debug("Try to write data to OSM without a changeset") 44 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") 45 | try: 46 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 47 | except osmapi.NoChangesetOpenError as e: 48 | log.exception(e) 49 | log.debug("There is no open changeset") 50 | input("Press Enter to continue...") 51 | clear_screen() 52 | 53 | 54 | # wrong server: ConnectionError 55 | log.debug("Connect to wrong server...") 56 | api = osmapi.OsmApi(api="https://invalid.server.name") 57 | try: 58 | api.ChangesetGet(123) 59 | except osmapi.ConnectionApiError as e: 60 | log.exception(e) 61 | log.debug("Error connecting to server") 62 | input("Press Enter to continue...") 63 | clear_screen() 64 | 65 | 66 | # changeset not found: ElementNotFoundApiError 67 | log.debug("Request non-existent changeset id...") 68 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") 69 | try: 70 | api.ChangesetGet(111111111111) 71 | except osmapi.ElementNotFoundApiError as e: 72 | log.exception(e) 73 | log.debug("Changeset not found") 74 | input("Press Enter to continue...") 75 | clear_screen() 76 | 77 | 78 | # unauthorized request 79 | log.debug("Try to add data with wrong authorization") 80 | try: 81 | s = requests.Session() 82 | s.auth = ("user", "pass") 83 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", session=s) 84 | with api.Changeset({"comment": "My first test"}) as changeset_id: 85 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 86 | except osmapi.UnauthorizedApiError as e: 87 | log.exception(e) 88 | log.debug("Unauthorized to make this request") 89 | input("Press Enter to continue...") 90 | clear_screen() 91 | 92 | # request without auhorization 93 | log.debug("Try to add data without authorization") 94 | try: 95 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") 96 | with api.Changeset({"comment": "My first test"}) as changeset_id: 97 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 98 | except osmapi.UsernamePasswordMissingError as e: 99 | log.exception(e) 100 | log.debug("Username/Password or authorization missing") 101 | input("Press Enter to continue...") 102 | clear_screen() 103 | 104 | 105 | # a more or less complete "real-life" example 106 | client_id = os.getenv("OSM_OAUTH_CLIENT_ID") 107 | client_secret = os.getenv("OSM_OAUTH_CLIENT_SECRET") 108 | 109 | try: 110 | auth = OpenStreetMapDevAuth( 111 | client_id, client_secret, ["write_api", "write_notes"] 112 | ).auth_code() 113 | except OAuth2Error as e: 114 | log.exception(e) 115 | log.debug("An OAuth2 error occured") 116 | sys.exit(1) 117 | 118 | try: 119 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", session=auth.session) 120 | with api.Changeset({"comment": "My first test"}) as changeset_id: 121 | log.debug(f"Part of Changeset {changeset_id}") 122 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 123 | log.debug(node1) 124 | 125 | # get all the info from the closed changeset 126 | changeset = api.ChangesetGet(changeset_id) 127 | log.debug(changeset) 128 | exit_code = 0 129 | except osmapi.ConnectionApiError as e: 130 | log.debug(f"Connection error: {str(e)}") 131 | exit_code = 1 132 | # display error for user, try again? 133 | except osmapi.ElementNotFoundApiError as e: 134 | log.debug(f"Changeset not found: {str(e)}") 135 | exit_code = 1 136 | except osmapi.ApiError as e: 137 | log.debug(f"Error on the API side: {str(e)}") 138 | exit_code = 1 139 | except osmapi.OsmApiError as e: 140 | log.debug(f"Some other error: {str(e)}") 141 | exit_code = 1 142 | finally: 143 | log.debug(f"Exit code: {exit_code}") 144 | sys.exit(exit_code) 145 | -------------------------------------------------------------------------------- /examples/log_output.py: -------------------------------------------------------------------------------- 1 | import osmapi 2 | import logging 3 | from pprint import pformat 4 | import urllib3 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | loglevel = logging.DEBUG 9 | logging.basicConfig( 10 | format="%(asctime)s %(name)s %(levelname)-8s %(message)s", 11 | level=loglevel, 12 | datefmt="%Y-%m-%d %H:%M:%S", 13 | ) 14 | logging.captureWarnings(True) 15 | 16 | # shut up DEBUG messages of specific loggers 17 | logging.getLogger(osmapi.dom.__name__).setLevel(logging.INFO) 18 | logging.getLogger(urllib3.__name__).setLevel(logging.INFO) 19 | 20 | 21 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") 22 | node1 = api.NodeGet("1111") 23 | log.debug(pformat(node1)) 24 | -------------------------------------------------------------------------------- /examples/notes.py: -------------------------------------------------------------------------------- 1 | import osmapi 2 | from dotenv import load_dotenv, find_dotenv 3 | import os 4 | from pprint import pprint 5 | 6 | load_dotenv(find_dotenv()) 7 | user = os.getenv("OSM_USER") 8 | pw = os.getenv("OSM_PASS") 9 | 10 | api = osmapi.OsmApi( 11 | api="https://api06.dev.openstreetmap.org", username=user, password=pw 12 | ) 13 | empty_notes = api.NotesGet( 14 | -93.8472901, 35.9763601, -80, 36.176360100000004, limit=1, closed=0 15 | ) 16 | pprint(empty_notes) 17 | 18 | 19 | # create note and then search for it 20 | note = api.NoteCreate( 21 | { 22 | "lat": 47.3383501, 23 | "lon": 8.5339522, 24 | "text": "test note", 25 | } 26 | ) 27 | test_notes = api.NotesGet(8.527504, 47.337063, 8.540679, 47.341673, limit=1, closed=0) 28 | pprint(test_notes) 29 | 30 | 31 | api.NoteComment(note["id"], "Another comment") 32 | api.NoteClose(note["id"], "Close this test note") 33 | 34 | 35 | # try to close an already closed note 36 | try: 37 | api.NoteClose(note["id"], "Close the note again") 38 | except osmapi.NoteAlreadyClosedApiError: 39 | print("") 40 | print(f"The note {note['id']} has already been closed") 41 | 42 | # try to comment on closed note 43 | try: 44 | api.NoteComment(note["id"], "Just a comment") 45 | except osmapi.NoteAlreadyClosedApiError: 46 | print("") 47 | print(f"The note {note['id']} is closed, comment no longer possible") 48 | -------------------------------------------------------------------------------- /examples/oauth2.py: -------------------------------------------------------------------------------- 1 | # install oauthlib for requests: pip install requests-oauth2client 2 | from requests_oauth2client import OAuth2Client, OAuth2AuthorizationCodeAuth 3 | import requests 4 | import webbrowser 5 | import osmapi 6 | from dotenv import load_dotenv, find_dotenv 7 | import os 8 | 9 | load_dotenv(find_dotenv()) 10 | 11 | # Credentials you get from registering a new application 12 | # register here: https://master.apis.dev.openstreetmap.org/oauth2/applications 13 | # or on production: https://www.openstreetmap.org/oauth2/applications 14 | client_id = os.getenv("OSM_OAUTH_CLIENT_ID") 15 | client_secret = os.getenv("OSM_OAUTH_CLIENT_SECRET") 16 | 17 | # special value for redirect_uri for non-web applications 18 | redirect_uri = "urn:ietf:wg:oauth:2.0:oob" 19 | 20 | authorization_base_url = "https://master.apis.dev.openstreetmap.org/oauth2/authorize" 21 | token_url = "https://master.apis.dev.openstreetmap.org/oauth2/token" 22 | 23 | oauth2client = OAuth2Client( 24 | token_endpoint=token_url, 25 | authorization_endpoint=authorization_base_url, 26 | redirect_uri=redirect_uri, 27 | auth=(client_id, client_secret), 28 | code_challenge_method=None, 29 | ) 30 | 31 | # open OSM website to authrorize user using the write_api and write_notes scope 32 | scope = ["write_api", "write_notes"] 33 | az_request = oauth2client.authorization_request(scope=scope) 34 | print(f"Authorize user using this URL: {az_request.uri}") 35 | webbrowser.open(az_request.uri) 36 | 37 | # create a new requests session using the OAuth authorization 38 | auth_code = input("Paste the authorization code here: ") 39 | auth = OAuth2AuthorizationCodeAuth( 40 | oauth2client, 41 | auth_code, 42 | redirect_uri=redirect_uri, 43 | ) 44 | oauth_session = requests.Session() 45 | oauth_session.auth = auth 46 | 47 | # use the custom session 48 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", session=oauth_session) 49 | with api.Changeset({"comment": "My first test"}) as changeset_id: 50 | print(f"Part of Changeset {changeset_id}") 51 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 52 | print(node1) 53 | -------------------------------------------------------------------------------- /examples/oauth2_backend.py: -------------------------------------------------------------------------------- 1 | # This script shows how to authenticate with OAuth2 with a backend application 2 | # The token is saved to disk in $HOME/.osmapi/token.json 3 | # It can be reused until it's revoked or expired. 4 | 5 | # install oauthlib for requests: pip install oauthlib requests-oauthlib 6 | from requests_oauthlib import OAuth2Session 7 | import json 8 | import webbrowser 9 | import osmapi 10 | from dotenv import load_dotenv, find_dotenv 11 | import os 12 | import sys 13 | 14 | load_dotenv(find_dotenv()) 15 | 16 | # Credentials you get from registering a new application 17 | # register here: https://master.apis.dev.openstreetmap.org/oauth2/applications 18 | # or on production: https://www.openstreetmap.org/oauth2/applications 19 | client_id = os.getenv("OSM_OAUTH_CLIENT_ID") 20 | client_secret = os.getenv("OSM_OAUTH_CLIENT_SECRET") 21 | 22 | # special value for redirect_uri for non-web applications 23 | redirect_uri = "urn:ietf:wg:oauth:2.0:oob" 24 | 25 | authorization_base_url = "https://master.apis.dev.openstreetmap.org/oauth2/authorize" 26 | token_url = "https://master.apis.dev.openstreetmap.org/oauth2/token" 27 | scope = ["write_api", "write_notes"] 28 | 29 | 30 | def get_osmapi_path(): 31 | base_dir = "" 32 | 33 | if os.getenv("HOME"): 34 | base_dir = os.getenv("HOME") 35 | elif os.getenv("HOMEDRIVE") and os.getenv("HOMEPATH"): 36 | base_dir = os.path.join(os.getenv("HOMEDRIVE"), os.getenv("HOMEPATH")) 37 | elif os.getenv("USERPROFILE"): 38 | base_dir = os.getenv("USERPROFILE") 39 | 40 | if not base_dir: 41 | print( 42 | "Unable to find home directory (check env vars HOME, HOMEDRIVE, HOMEPATH and USERPROFILE)", # noqa 43 | file=sys.stderr, 44 | ) 45 | raise Exception("Home directory not found") 46 | 47 | return os.path.join(base_dir, ".osmapi") 48 | 49 | 50 | def token_saver(token): 51 | osmapi_path = get_osmapi_path() 52 | token_path = os.path.join(osmapi_path, "token.json") 53 | 54 | with open(token_path, "w") as f: 55 | print(f"Saving token {token} to {token_path}") 56 | f.write(json.dumps(token)) 57 | 58 | 59 | def token_loader(): 60 | osmapi_path = get_osmapi_path() 61 | token_path = os.path.join(osmapi_path, "token.json") 62 | 63 | with open(token_path, "r") as f: 64 | token = json.loads(f.read()) 65 | print(f"Loaded token {token} from {token_path}") 66 | return token 67 | 68 | 69 | def save_and_get_access_token(client_id, client_secret, redirect_uri, scope): 70 | oauth = OAuth2Session( 71 | client_id=client_id, 72 | redirect_uri=redirect_uri, 73 | scope=scope, 74 | ) 75 | 76 | login_url, _ = oauth.authorization_url(authorization_base_url) 77 | 78 | print(f"Authorize user using this URL: {login_url}") 79 | webbrowser.open(login_url) 80 | 81 | authorization_code = input("Paste the authorization code here: ") 82 | 83 | token = oauth.fetch_token( 84 | token_url=token_url, 85 | client_secret=client_secret, 86 | code=authorization_code, 87 | ) 88 | 89 | token_saver(token) 90 | return token 91 | 92 | 93 | def make_osm_change(oauth_session): 94 | api = osmapi.OsmApi( 95 | api="https://api06.dev.openstreetmap.org", session=oauth_session 96 | ) 97 | with api.Changeset({"comment": "My first test"}) as changeset_id: 98 | print(f"Part of Changeset {changeset_id}") 99 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 100 | print(node1) 101 | 102 | 103 | # load a previously saved token 104 | try: 105 | token = token_loader() 106 | except FileNotFoundError: 107 | print("Token not found, get a new one...") 108 | token = save_and_get_access_token(client_id, client_secret, redirect_uri, scope) 109 | 110 | # test the token 111 | try: 112 | oauth_session = OAuth2Session(client_id, token=token) 113 | make_osm_change(oauth_session) 114 | except osmapi.errors.UnauthorizedApiError: 115 | print("Token expired, let's create a new one") 116 | token = save_and_get_access_token(client_id, client_secret, redirect_uri, scope) 117 | oauth_session = OAuth2Session(client_id, token=token) 118 | make_osm_change(oauth_session) 119 | -------------------------------------------------------------------------------- /examples/timeout.py: -------------------------------------------------------------------------------- 1 | # install cli-oauth2 for requests: pip install cli-oauth2 2 | from oauthcli import OpenStreetMapDevAuth 3 | import osmapi 4 | from dotenv import load_dotenv, find_dotenv 5 | import os 6 | 7 | load_dotenv(find_dotenv()) 8 | 9 | # load secrets for OAuth 10 | client_id = os.getenv("OSM_OAUTH_CLIENT_ID") 11 | client_secret = os.getenv("OSM_OAUTH_CLIENT_SECRET") 12 | 13 | auth = OpenStreetMapDevAuth( 14 | client_id, client_secret, ["write_api", "write_notes"] 15 | ).auth_code() 16 | 17 | 18 | # Use a normal timeout (30s is the default value) 19 | normal_timeout_api = osmapi.OsmApi( 20 | api="https://api06.dev.openstreetmap.org", session=auth.session, timeout=30 21 | ) 22 | changeset_id = normal_timeout_api.ChangesetCreate({"comment": "My first test"}) 23 | print(f"Create new changeset {changeset_id}") 24 | 25 | # Deliberately using a very small timeout to show what happens when a timeout occurs 26 | low_timeout_api = osmapi.OsmApi( 27 | api="https://api06.dev.openstreetmap.org", session=auth.session, timeout=0.00001 28 | ) 29 | try: 30 | changeset_id = low_timeout_api.ChangesetCreate({"comment": "My first test"}) 31 | print(f"Create new changeset {changeset_id}") 32 | except osmapi.errors.TimeoutApiError as e: 33 | print(f"Timeout error occured: {str(e)}") 34 | -------------------------------------------------------------------------------- /examples/write_to_osm.py: -------------------------------------------------------------------------------- 1 | # install cli-oauth2 for requests: pip install cli-oauth2 2 | from oauthcli import OpenStreetMapDevAuth 3 | import osmapi 4 | from dotenv import load_dotenv, find_dotenv 5 | import os 6 | 7 | load_dotenv(find_dotenv()) 8 | 9 | # load secrets for OAuth 10 | client_id = os.getenv("OSM_OAUTH_CLIENT_ID") 11 | client_secret = os.getenv("OSM_OAUTH_CLIENT_SECRET") 12 | 13 | auth = OpenStreetMapDevAuth( 14 | client_id, client_secret, ["write_api", "write_notes"] 15 | ).auth_code() 16 | 17 | 18 | api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", session=auth.session) 19 | with api.Changeset({"comment": "My first test"}) as changeset_id: 20 | print(f"Part of Changeset {changeset_id}") 21 | node1 = api.NodeCreate({"lon": 1, "lat": 1, "tag": {}}) 22 | print(node1) 23 | node2 = api.NodeCreate({"lon": 2, "lat": 2, "tag": {}}) 24 | print(node2) 25 | way = api.WayCreate( 26 | { 27 | "nd": [ 28 | node1["id"], 29 | node2["id"], 30 | ], 31 | "tag": { 32 | "highway": "unclassified", 33 | "name": "Osmapi Street", 34 | }, 35 | } 36 | ) 37 | print(way) 38 | -------------------------------------------------------------------------------- /osmapi/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.3.0" 2 | 3 | from .OsmApi import * # noqa 4 | from .errors import * # noqa 5 | -------------------------------------------------------------------------------- /osmapi/dom.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import xml.dom.minidom 3 | import xml.parsers.expat 4 | import logging 5 | 6 | from . import errors 7 | from . import xmlbuilder 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def OsmResponseToDom(response, tag, single=False, allow_empty=False): 14 | """ 15 | Returns the (sub-) DOM parsed from an OSM response 16 | """ 17 | try: 18 | dom = xml.dom.minidom.parseString(response) 19 | osm_dom = dom.getElementsByTagName("osm")[0] 20 | all_data = osm_dom.getElementsByTagName(tag) 21 | first_element = all_data[0] 22 | except IndexError as e: 23 | if allow_empty: 24 | return [] 25 | raise errors.XmlResponseInvalidError( 26 | f"The XML response from the OSM API is invalid: {e!r}" 27 | ) 28 | except xml.parsers.expat.ExpatError as e: 29 | raise errors.XmlResponseInvalidError( 30 | f"The XML response from the OSM API is invalid: {e!r}" 31 | ) 32 | 33 | if single: 34 | return first_element 35 | return all_data 36 | 37 | 38 | def DomParseNode(DomElement): 39 | """ 40 | Returns NodeData for the node. 41 | """ 42 | result = _DomGetAttributes(DomElement) 43 | result["tag"] = _DomGetTag(DomElement) 44 | return result 45 | 46 | 47 | def DomParseWay(DomElement): 48 | """ 49 | Returns WayData for the way. 50 | """ 51 | result = _DomGetAttributes(DomElement) 52 | result["tag"] = _DomGetTag(DomElement) 53 | result["nd"] = _DomGetNd(DomElement) 54 | return result 55 | 56 | 57 | def DomParseRelation(DomElement): 58 | """ 59 | Returns RelationData for the relation. 60 | """ 61 | result = _DomGetAttributes(DomElement) 62 | result["tag"] = _DomGetTag(DomElement) 63 | result["member"] = _DomGetMember(DomElement) 64 | return result 65 | 66 | 67 | def DomParseChangeset(DomElement, include_discussion=False): 68 | """ 69 | Returns ChangesetData for the changeset. 70 | """ 71 | result = _DomGetAttributes(DomElement) 72 | result["tag"] = _DomGetTag(DomElement) 73 | if include_discussion: 74 | result["discussion"] = _DomGetDiscussion(DomElement) 75 | 76 | return result 77 | 78 | 79 | def DomParseNote(DomElement): 80 | """ 81 | Returns NoteData for the note. 82 | """ 83 | result = _DomGetAttributes(DomElement) 84 | result["id"] = xmlbuilder._GetXmlValue(DomElement, "id") 85 | result["status"] = xmlbuilder._GetXmlValue(DomElement, "status") 86 | 87 | result["date_created"] = _ParseDate( 88 | xmlbuilder._GetXmlValue(DomElement, "date_created") 89 | ) 90 | result["date_closed"] = _ParseDate( 91 | xmlbuilder._GetXmlValue(DomElement, "date_closed") 92 | ) 93 | result["comments"] = _DomGetComments(DomElement) 94 | 95 | return result 96 | 97 | 98 | def _DomGetAttributes(DomElement): 99 | """ 100 | Returns a formated dictionnary of attributes of a DomElement. 101 | """ 102 | 103 | def is_true(v): 104 | return v == "true" 105 | 106 | attribute_mapping = { 107 | "uid": int, 108 | "changeset": int, 109 | "version": int, 110 | "id": int, 111 | "lat": float, 112 | "lon": float, 113 | "open": is_true, 114 | "visible": is_true, 115 | "ref": int, 116 | "comments_count": int, 117 | "timestamp": _ParseDate, 118 | "created_at": _ParseDate, 119 | "closed_at": _ParseDate, 120 | "date": _ParseDate, 121 | } 122 | result = {} 123 | for k, v in DomElement.attributes.items(): 124 | try: 125 | result[k] = attribute_mapping[k](v) 126 | except KeyError: 127 | result[k] = v 128 | return result 129 | 130 | 131 | def _DomGetTag(DomElement): 132 | """ 133 | Returns the dictionnary of tags of a DomElement. 134 | """ 135 | result = {} 136 | for t in DomElement.getElementsByTagName("tag"): 137 | k = t.attributes["k"].value 138 | v = t.attributes["v"].value 139 | result[k] = v 140 | return result 141 | 142 | 143 | def _DomGetNd(DomElement): 144 | """ 145 | Returns the list of nodes of a DomElement. 146 | """ 147 | result = [] 148 | for t in DomElement.getElementsByTagName("nd"): 149 | result.append(int(int(t.attributes["ref"].value))) 150 | return result 151 | 152 | 153 | def _DomGetDiscussion(DomElement): 154 | """ 155 | Returns the dictionnary of comments of a DomElement. 156 | """ 157 | result = [] 158 | try: 159 | discussion = DomElement.getElementsByTagName("discussion")[0] 160 | for t in discussion.getElementsByTagName("comment"): 161 | comment = _DomGetAttributes(t) 162 | comment["text"] = xmlbuilder._GetXmlValue(t, "text") 163 | result.append(comment) 164 | except IndexError: 165 | pass 166 | return result 167 | 168 | 169 | def _DomGetComments(DomElement): 170 | """ 171 | Returns the list of comments of a DomElement. 172 | """ 173 | result = [] 174 | for t in DomElement.getElementsByTagName("comment"): 175 | comment = {} 176 | comment["date"] = _ParseDate(xmlbuilder._GetXmlValue(t, "date")) 177 | comment["action"] = xmlbuilder._GetXmlValue(t, "action") 178 | comment["text"] = xmlbuilder._GetXmlValue(t, "text") 179 | comment["html"] = xmlbuilder._GetXmlValue(t, "html") 180 | comment["uid"] = xmlbuilder._GetXmlValue(t, "uid") 181 | comment["user"] = xmlbuilder._GetXmlValue(t, "user") 182 | result.append(comment) 183 | return result 184 | 185 | 186 | def _DomGetMember(DomElement): 187 | """ 188 | Returns a list of relation members. 189 | """ 190 | result = [] 191 | for m in DomElement.getElementsByTagName("member"): 192 | result.append(_DomGetAttributes(m)) 193 | return result 194 | 195 | 196 | def _ParseDate(DateString): 197 | date_formats = ["%Y-%m-%d %H:%M:%S UTC", "%Y-%m-%dT%H:%M:%SZ"] 198 | for date_format in date_formats: 199 | try: 200 | result = datetime.strptime(DateString, date_format) 201 | return result 202 | except (ValueError, TypeError): 203 | logger.debug(f"{DateString} does not match {date_format}") 204 | 205 | return DateString 206 | -------------------------------------------------------------------------------- /osmapi/errors.py: -------------------------------------------------------------------------------- 1 | class OsmApiError(Exception): 2 | """ 3 | General OsmApi error class to provide a superclass for all other errors 4 | """ 5 | 6 | 7 | class MaximumRetryLimitReachedError(OsmApiError): 8 | """ 9 | Error when the maximum amount of retries is reached and we have to give up 10 | """ 11 | 12 | 13 | class UsernamePasswordMissingError(OsmApiError): 14 | """ 15 | Error when username or password is missing for an authenticated request 16 | """ 17 | 18 | pass 19 | 20 | 21 | class NoChangesetOpenError(OsmApiError): 22 | """ 23 | Error when an operation requires an open changeset, but currently 24 | no changeset _is_ open 25 | """ 26 | 27 | pass 28 | 29 | 30 | class ChangesetAlreadyOpenError(OsmApiError): 31 | """ 32 | Error when a user tries to open a changeset when there is already 33 | an open changeset 34 | """ 35 | 36 | pass 37 | 38 | 39 | class OsmTypeAlreadyExistsError(OsmApiError): 40 | """ 41 | Error when a user tries to create an object that already exsits 42 | """ 43 | 44 | pass 45 | 46 | 47 | class XmlResponseInvalidError(OsmApiError): 48 | """ 49 | Error if the XML response from the OpenStreetMap API is invalid 50 | """ 51 | 52 | 53 | class ApiError(OsmApiError): 54 | """ 55 | Error class, is thrown when an API request fails 56 | """ 57 | 58 | def __init__(self, status, reason, payload): 59 | self.status = status 60 | """HTTP error code""" 61 | 62 | self.reason = reason 63 | """Error message""" 64 | 65 | self.payload = payload 66 | """Payload of API when this error occured""" 67 | 68 | def __str__(self): 69 | return f"Request failed: {self.status} - {self.reason} - {self.payload}" 70 | 71 | 72 | class UnauthorizedApiError(ApiError): 73 | """ 74 | Error when the API returned an Unauthorized error, 75 | e.g. when the provided OAuth token is expired 76 | """ 77 | 78 | pass 79 | 80 | 81 | class AlreadySubscribedApiError(ApiError): 82 | """ 83 | Error when a user tries to subscribe to a changeset 84 | that she is already subscribed to 85 | """ 86 | 87 | pass 88 | 89 | 90 | class NotSubscribedApiError(ApiError): 91 | """ 92 | Error when user tries to unsubscribe from a changeset 93 | that he is not subscribed to 94 | """ 95 | 96 | pass 97 | 98 | 99 | class ElementDeletedApiError(ApiError): 100 | """ 101 | Error when the requested element is deleted 102 | """ 103 | 104 | pass 105 | 106 | 107 | class ElementNotFoundApiError(ApiError): 108 | """ 109 | Error if the the requested element was not found 110 | """ 111 | 112 | 113 | class ResponseEmptyApiError(ApiError): 114 | """ 115 | Error when the response to the request is empty 116 | """ 117 | 118 | pass 119 | 120 | 121 | class ChangesetClosedApiError(ApiError): 122 | """ 123 | Error if the the changeset in question has already been closed 124 | """ 125 | 126 | 127 | class NoteAlreadyClosedApiError(ApiError): 128 | """ 129 | Error if the the note in question has already been closed 130 | """ 131 | 132 | 133 | class VersionMismatchApiError(ApiError): 134 | """ 135 | Error if the provided version does not match the database version 136 | of the element 137 | """ 138 | 139 | 140 | class PreconditionFailedApiError(ApiError): 141 | """ 142 | Error if the precondition of the operation was not met: 143 | - When a way has nodes that do not exist or are not visible 144 | - When a relation has elements that do not exist or are not visible 145 | - When a node/way/relation is still used in a way/relation 146 | """ 147 | 148 | 149 | class TimeoutApiError(ApiError): 150 | """ 151 | Error if the http request ran into a timeout 152 | """ 153 | 154 | 155 | class ConnectionApiError(ApiError): 156 | """ 157 | Error if there was a network error (e.g. DNS failure, refused connection) 158 | while connecting to the remote server. 159 | """ 160 | -------------------------------------------------------------------------------- /osmapi/http.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools as it 3 | import logging 4 | import requests 5 | import time 6 | 7 | from . import errors 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class OsmApiSession: 14 | MAX_RETRY_LIMIT = 5 15 | """Maximum retries if a call to the remote API fails (default: 5)""" 16 | 17 | def __init__(self, base_url, created_by, auth=None, session=None, timeout=30): 18 | self._api = base_url 19 | self._created_by = created_by 20 | self._timeout = timeout 21 | 22 | try: 23 | self._auth = auth 24 | if not auth and session.auth: 25 | self._auth = session.auth 26 | except AttributeError: 27 | pass 28 | 29 | self._http_session = session 30 | self._session = self._get_http_session() 31 | 32 | def close(self): 33 | if self._session: 34 | self._session.close() 35 | 36 | def _http_request(self, method, path, auth, send, return_value=True): # noqa 37 | """ 38 | Returns the response generated by an HTTP request. 39 | 40 | `method` is a HTTP method to be executed 41 | with the request data. For example: 'GET' or 'POST'. 42 | `path` is the path to the requested resource relative to the 43 | base API address stored in self._api. Should start with a 44 | slash character to separate the URL. 45 | `auth` is a boolean indicating whether authentication should 46 | be preformed on this request. 47 | `send` contains additional data that might be sent in a 48 | request. 49 | `return_value` indicates wheter this request should return 50 | any data or not. 51 | 52 | If the username or password is missing, 53 | `OsmApi.UsernamePasswordMissingError` is raised. 54 | 55 | If the requested element has been deleted, 56 | `OsmApi.ElementDeletedApiError` is raised. 57 | 58 | If the requested element can not be found, 59 | `OsmApi.ElementNotFoundApiError` is raised. 60 | 61 | If the response status code indicates an error, 62 | `OsmApi.ApiError` is raised. 63 | """ 64 | logger.debug(f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {method} {path}") 65 | 66 | # Add API base URL to path 67 | path = self._api + path 68 | 69 | if auth and not self._auth: 70 | raise errors.UsernamePasswordMissingError("Username/Password missing") 71 | 72 | try: 73 | response = self._session.request( 74 | method, path, data=send, timeout=self._timeout 75 | ) 76 | except requests.exceptions.Timeout as e: 77 | raise errors.TimeoutApiError( 78 | 0, f"Request timed out (timeout={self._timeout})", "" 79 | ) from e 80 | except requests.exceptions.ConnectionError as e: 81 | raise errors.ConnectionApiError(0, f"Connection error: {str(e)}", "") from e 82 | except requests.exceptions.RequestException as e: 83 | raise errors.ApiError(0, str(e), "") from e 84 | 85 | if response.status_code != 200: 86 | payload = response.content.strip() 87 | if response.status_code == 401: 88 | raise errors.UnauthorizedApiError( 89 | response.status_code, response.reason, payload 90 | ) 91 | if response.status_code == 404: 92 | raise errors.ElementNotFoundApiError( 93 | response.status_code, response.reason, payload 94 | ) 95 | elif response.status_code == 410: 96 | raise errors.ElementDeletedApiError( 97 | response.status_code, response.reason, payload 98 | ) 99 | raise errors.ApiError(response.status_code, response.reason, payload) 100 | if return_value and not response.content: 101 | raise errors.ResponseEmptyApiError( 102 | response.status_code, response.reason, "" 103 | ) 104 | 105 | logger.debug(f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {method} {path}") 106 | return response.content 107 | 108 | def _http(self, cmd, path, auth, send, return_value=True): # noqa 109 | for i in it.count(1): 110 | try: 111 | return self._http_request( 112 | cmd, path, auth, send, return_value=return_value 113 | ) 114 | except errors.ApiError as e: 115 | if e.status >= 500: 116 | if i == self.MAX_RETRY_LIMIT: 117 | raise 118 | if i != 1: 119 | self._sleep() 120 | self._session = self._get_http_session() 121 | else: 122 | logger.exception("ApiError Exception occured") 123 | raise 124 | except errors.UsernamePasswordMissingError: 125 | raise 126 | except Exception as e: 127 | logger.exception("General exception occured") 128 | if i == self.MAX_RETRY_LIMIT: 129 | if isinstance(e, errors.OsmApiError): 130 | raise 131 | raise errors.MaximumRetryLimitReachedError( 132 | f"Give up after {i} retries" 133 | ) from e 134 | if i != 1: 135 | self._sleep() 136 | self._session = self._get_http_session() 137 | 138 | def _get_http_session(self): 139 | """ 140 | Creates a requests session for connection pooling. 141 | """ 142 | if self._http_session: 143 | session = self._http_session 144 | else: 145 | session = requests.Session() 146 | 147 | session.auth = self._auth 148 | session.headers.update({"user-agent": self._created_by}) 149 | return session 150 | 151 | def _sleep(self): 152 | time.sleep(5) 153 | 154 | def _get(self, path): 155 | return self._http("GET", path, False, None) 156 | 157 | def _put(self, path, data, return_value=True): 158 | return self._http("PUT", path, True, data, return_value=return_value) 159 | 160 | def _post(self, path, data, optionalAuth=False, forceAuth=False): 161 | # the Notes API allows certain POSTs by non-authenticated users 162 | auth = optionalAuth and self._auth 163 | if forceAuth: 164 | auth = True 165 | return self._http("POST", path, auth, data) 166 | 167 | def _delete(self, path, data): 168 | return self._http("DELETE", path, True, data) 169 | -------------------------------------------------------------------------------- /osmapi/parser.py: -------------------------------------------------------------------------------- 1 | import xml.dom.minidom 2 | import xml.parsers.expat 3 | 4 | from . import errors 5 | from . import dom 6 | 7 | 8 | def ParseOsm(data): 9 | """ 10 | Parse osm data. 11 | 12 | Returns list of dict: 13 | 14 | #!python 15 | { 16 | type: node|way|relation, 17 | data: {} 18 | } 19 | """ 20 | try: 21 | data = xml.dom.minidom.parseString(data) 22 | data = data.getElementsByTagName("osm")[0] 23 | except (xml.parsers.expat.ExpatError, IndexError) as e: 24 | raise errors.XmlResponseInvalidError( 25 | f"The XML response from the OSM API is invalid: {e!r}" 26 | ) from e 27 | 28 | result = [] 29 | for elem in data.childNodes: 30 | if elem.nodeName == "node": 31 | result.append({"type": elem.nodeName, "data": dom.DomParseNode(elem)}) 32 | elif elem.nodeName == "way": 33 | result.append({"type": elem.nodeName, "data": dom.DomParseWay(elem)}) 34 | elif elem.nodeName == "relation": 35 | result.append({"type": elem.nodeName, "data": dom.DomParseRelation(elem)}) 36 | return result 37 | 38 | 39 | def ParseOsc(data): 40 | """ 41 | Parse osc data. 42 | 43 | Returns list of dict: 44 | 45 | #!python 46 | { 47 | type: node|way|relation, 48 | action: create|delete|modify, 49 | data: {} 50 | } 51 | """ 52 | try: 53 | data = xml.dom.minidom.parseString(data) 54 | data = data.getElementsByTagName("osmChange")[0] 55 | except (xml.parsers.expat.ExpatError, IndexError) as e: 56 | raise errors.XmlResponseInvalidError( 57 | f"The XML response from the OSM API is invalid: {e!r}" 58 | ) from e 59 | 60 | result = [] 61 | for action in data.childNodes: 62 | if action.nodeName == "#text": 63 | continue 64 | for elem in action.childNodes: 65 | if elem.nodeName == "node": 66 | result.append( 67 | { 68 | "action": action.nodeName, 69 | "type": elem.nodeName, 70 | "data": dom.DomParseNode(elem), 71 | } 72 | ) 73 | elif elem.nodeName == "way": 74 | result.append( 75 | { 76 | "action": action.nodeName, 77 | "type": elem.nodeName, 78 | "data": dom.DomParseWay(elem), 79 | } 80 | ) 81 | elif elem.nodeName == "relation": 82 | result.append( 83 | { 84 | "action": action.nodeName, 85 | "type": elem.nodeName, 86 | "data": dom.DomParseRelation(elem), 87 | } 88 | ) 89 | return result 90 | 91 | 92 | def ParseNotes(data): 93 | """ 94 | Parse notes data. 95 | 96 | Returns a list of dict: 97 | 98 | #!python 99 | [ 100 | { 101 | 'id': integer, 102 | 'action': opened|commented|closed, 103 | 'status': open|closed 104 | 'date_created': creation date 105 | 'date_closed': closing data|None 106 | 'uid': User ID|None 107 | 'user': User name|None 108 | 'comments': {} 109 | }, 110 | { ... } 111 | ] 112 | """ 113 | noteElements = dom.OsmResponseToDom(data, tag="note", allow_empty=True) 114 | result = [] 115 | for noteElement in noteElements: 116 | note = dom.DomParseNote(noteElement) 117 | result.append(note) 118 | return result 119 | -------------------------------------------------------------------------------- /osmapi/xmlbuilder.py: -------------------------------------------------------------------------------- 1 | def _XmlBuild(ElementType, ElementData, WithHeaders=True, data=None): # noqa 2 | xml = "" 3 | if WithHeaders: 4 | xml += '\n' 5 | xml += '\n' 7 | 8 | # 9 | xml += " <" + ElementType 10 | if "id" in ElementData: 11 | xml += ' id="' + str(ElementData["id"]) + '"' 12 | if "lat" in ElementData: 13 | xml += ' lat="' + str(ElementData["lat"]) + '"' 14 | if "lon" in ElementData: 15 | xml += ' lon="' + str(ElementData["lon"]) + '"' 16 | if "version" in ElementData: 17 | xml += ' version="' + str(ElementData["version"]) + '"' 18 | visible_str = str(ElementData.get("visible", True)).lower() 19 | xml += ' visible="' + visible_str + '"' 20 | if ElementType in ["node", "way", "relation"]: 21 | xml += ' changeset="' + str(data._CurrentChangesetId) + '"' 22 | xml += ">\n" 23 | 24 | # 25 | for k, v in ElementData.get("tag", {}).items(): 26 | xml += ' \n' 28 | 29 | # 30 | for member in ElementData.get("member", []): 31 | xml += ' 37 | for ref in ElementData.get("nd", []): 38 | xml += ' \n' 39 | 40 | # 41 | xml += " \n" 42 | 43 | if WithHeaders: 44 | xml += "\n" 45 | 46 | return xml.encode("utf8") 47 | 48 | 49 | def _XmlEncode(text): 50 | return ( 51 | text.replace("&", "&") 52 | .replace('"', """) 53 | .replace("<", "<") 54 | .replace(">", ">") 55 | ) 56 | 57 | 58 | def _GetXmlValue(DomElement, tag): 59 | try: 60 | elem = DomElement.getElementsByTagName(tag)[0] 61 | return elem.firstChild.nodeValue 62 | except Exception: 63 | return None 64 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pdoc==14.5.1 2 | Pygments==2.15.0 3 | requests==2.32.0 4 | python-dotenv 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [flake8] 5 | max-complexity = 10 6 | exclude = .git,.tox,__pycache__,pyenv,build,dist 7 | # set to true to check all ignored errors 8 | disable_noqa = False 9 | 10 | # adaptions for black 11 | max-line-length = 88 12 | extend-ignore = E203 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from codecs import open 4 | from setuptools import setup, find_packages 5 | import re 6 | 7 | with open("osmapi/__init__.py", "r") as fd: 8 | version = re.search( 9 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE 10 | ).group(1) 11 | 12 | if not version: 13 | raise RuntimeError("Cannot find version information") 14 | 15 | with open("README.md", "r", encoding="utf-8") as f: 16 | long_description = f.read() 17 | 18 | setup( 19 | name="osmapi", 20 | packages=find_packages(), 21 | version=version, 22 | install_requires=["requests"], 23 | python_requires=">=3.8", 24 | description="Python wrapper for the OSM API", 25 | long_description=long_description, 26 | long_description_content_type="text/markdown", 27 | author="Etienne Chové", 28 | author_email="chove@crans.org", 29 | maintainer="Stefan Oderbolz", 30 | maintainer_email="odi@metaodi.ch", 31 | url="https://github.com/metaodi/osmapi", 32 | download_url=f"https://github.com/metaodi/osmapi/archive/v{version}.zip", 33 | keywords=["openstreetmap", "osm", "api"], 34 | license="GPLv3", 35 | classifiers=[ 36 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 37 | "Intended Audience :: Developers", 38 | "Topic :: Scientific/Engineering :: GIS", 39 | "Topic :: Software Development :: Libraries", 40 | "Development Status :: 4 - Beta", 41 | "Programming Language :: Python :: 3", 42 | "Programming Language :: Python :: 3.8", 43 | "Programming Language :: Python :: 3.9", 44 | "Programming Language :: Python :: 3.10", 45 | "Programming Language :: Python :: 3.11", 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ ! -d pyenv ] && python -m venv pyenv 4 | source pyenv/bin/activate 5 | 6 | make deps 7 | pip install -e . 8 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | virtualenv 3 | xmltodict 4 | pytest 5 | pytest-cov 6 | responses 7 | coverage 8 | black 9 | pre-commit 10 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function cleanup { 6 | exit $? 7 | } 8 | 9 | trap "cleanup" EXIT 10 | 11 | # Check PEP-8 code style and McCabe complexity 12 | make lint 13 | 14 | # run tests 15 | make test 16 | 17 | # generate the docs 18 | make docs 19 | 20 | # setup a new virtualenv and try to install the lib 21 | virtualenv pyenv 22 | source pyenv/bin/activate && pip install . 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaodi/osmapi/0a1a398014dc49b0fd51fe26e016332074bfaa64/tests/__init__.py -------------------------------------------------------------------------------- /tests/capabilities_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | 3 | 4 | class TestOsmApiNode(osmapi_test.TestOsmApi): 5 | def test_Capabilities(self): 6 | self._session_mock() 7 | 8 | result = self.api.Capabilities() 9 | self.assertEqual( 10 | result, 11 | { 12 | "area": {"maximum": 0.25}, 13 | "changesets": {"maximum_elements": 50000.0}, 14 | "status": {"api": "mocked", "database": "online", "gpx": "online"}, 15 | "timeout": {"seconds": 300.0}, 16 | "tracepoints": {"per_page": 5000.0}, 17 | "version": {"maximum": 0.6, "minimum": 0.6}, 18 | "waynodes": {"maximum": 2000.0}, 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import osmapi 2 | import pytest 3 | from unittest import mock 4 | import responses 5 | import os 6 | import re 7 | 8 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 9 | 10 | 11 | @pytest.fixture 12 | def file_content(): 13 | def _file_content(filename): 14 | path = os.path.join(__location__, "fixtures", filename) 15 | if not os.path.exists(path): 16 | return "" 17 | with open(path) as f: 18 | return f.read() 19 | 20 | return _file_content 21 | 22 | 23 | @pytest.fixture 24 | def api(): 25 | api_base = "http://api06.dev.openstreetmap.org" 26 | api = osmapi.OsmApi(api=api_base) 27 | api._session._sleep = mock.Mock() 28 | 29 | yield api 30 | api.close() 31 | 32 | 33 | @pytest.fixture 34 | def auth_api(): 35 | api_base = "http://api06.dev.openstreetmap.org" 36 | api = osmapi.OsmApi(api=api_base, username="testuser", password="testpassword") 37 | api._session._sleep = mock.Mock() 38 | 39 | yield api 40 | api.close() 41 | 42 | 43 | @pytest.fixture 44 | def prod_api(): 45 | api_base = "https://www.openstreetmap.org" 46 | api = osmapi.OsmApi(api=api_base, username="testuser", password="testpassword") 47 | api._session._sleep = mock.Mock() 48 | 49 | yield api 50 | api.close() 51 | 52 | 53 | @pytest.fixture 54 | def mocked_responses(): 55 | with responses.RequestsMock() as rsps: 56 | yield rsps 57 | 58 | 59 | @pytest.fixture 60 | def add_response(mocked_responses, file_content, request): 61 | def _add_response(method, path=None, filename=None, body=None, status=200): 62 | if not filename: 63 | # use testname by default 64 | filename = f"{request.node.originalname}.xml" 65 | 66 | if path: 67 | url = f"http://api06.dev.openstreetmap.org/api/0.6{path}" 68 | else: 69 | url = re.compile(r"http:\/\/api06\.dev\.openstreetmap\.org.*") 70 | 71 | if not body: 72 | body = file_content(filename) 73 | mocked_responses.add(method, url=url, body=body, status=status) 74 | return mocked_responses 75 | 76 | return _add_response 77 | -------------------------------------------------------------------------------- /tests/dom_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | import osmapi 3 | from unittest import mock 4 | import datetime 5 | 6 | 7 | class TestOsmApiDom(osmapi_test.TestOsmApi): 8 | def test_DomGetAttributes(self): 9 | mock_domelement = mock.Mock() 10 | mock_domelement.attributes = { 11 | "uid": "12345", 12 | "open": "false", 13 | "visible": "true", 14 | "lat": "47.1234", 15 | "date": "2021-12-10T21:28:03Z", 16 | "new_attribute": "Test 123", 17 | } 18 | 19 | result = osmapi.dom._DomGetAttributes(mock_domelement) 20 | 21 | self.assertIsInstance(result, dict) 22 | self.assertEqual(result["uid"], 12345) 23 | self.assertEqual(result["open"], False) 24 | self.assertEqual(result["visible"], True) 25 | self.assertEqual(result["lat"], 47.1234) 26 | self.assertEqual(result["date"], datetime.datetime(2021, 12, 10, 21, 28, 3)) 27 | self.assertEqual(result["new_attribute"], "Test 123") 28 | 29 | def test_ParseDate(self): 30 | self.assertEqual( 31 | osmapi.dom._ParseDate("2021-02-25T09:49:33Z"), 32 | datetime.datetime(2021, 2, 25, 9, 49, 33), 33 | ) 34 | self.assertEqual( 35 | osmapi.dom._ParseDate("2021-02-25 09:49:33 UTC"), 36 | datetime.datetime(2021, 2, 25, 9, 49, 33), 37 | ) 38 | with self.assertLogs("osmapi.dom", level="DEBUG") as cm: 39 | self.assertEqual(osmapi.dom._ParseDate("2021-02-25"), "2021-02-25") 40 | self.assertEqual(osmapi.dom._ParseDate(""), "") 41 | self.assertIsNone(osmapi.dom._ParseDate(None)) 42 | 43 | # test logging output 44 | self.assertEqual( 45 | cm.output, 46 | [ 47 | "DEBUG:osmapi.dom:2021-02-25 does not match %Y-%m-%d %H:%M:%S UTC", 48 | "DEBUG:osmapi.dom:2021-02-25 does not match %Y-%m-%dT%H:%M:%SZ", 49 | "DEBUG:osmapi.dom: does not match %Y-%m-%d %H:%M:%S UTC", 50 | "DEBUG:osmapi.dom: does not match %Y-%m-%dT%H:%M:%SZ", 51 | "DEBUG:osmapi.dom:None does not match %Y-%m-%d %H:%M:%S UTC", 52 | "DEBUG:osmapi.dom:None does not match %Y-%m-%dT%H:%M:%SZ", 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /tests/fixtures/passwordfile.txt: -------------------------------------------------------------------------------- 1 | testosm:testpass 2 | testuser:testuserpass 3 | -------------------------------------------------------------------------------- /tests/fixtures/passwordfile_colon.txt: -------------------------------------------------------------------------------- 1 | testosm:testpass 2 | testuser:test:userpass 3 | -------------------------------------------------------------------------------- /tests/fixtures/test_Capabilities.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetClose.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaodi/osmapi/0a1a398014dc49b0fd51fe26e016332074bfaa64/tests/fixtures/test_ChangesetClose.xml -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetComment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetCreate.xml: -------------------------------------------------------------------------------- 1 | 4321 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetCreate_with_created_by.xml: -------------------------------------------------------------------------------- 1 | 1234 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetCreate_with_open_changeset.xml: -------------------------------------------------------------------------------- 1 | 4444 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetDownload.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetDownloadContainingUnicode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetDownload_invalid_response.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetGetWithComment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | test 9 | 10 | 11 | another comment 12 | 13 | 14 | hello 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetGetWithoutDiscussion.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetSubscribe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetSubscribeWhenAlreadySubscribed.xml: -------------------------------------------------------------------------------- 1 | You are already subscribed to changeset 52924. 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUnsubscribe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUnsubscribeWhenNotSubscribed.xml: -------------------------------------------------------------------------------- 1 | You are not subscribed to changeset 52924. 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpdate.xml: -------------------------------------------------------------------------------- 1 | 4444 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpdate_with_created_by.xml: -------------------------------------------------------------------------------- 1 | 4444 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpdate_wo_changeset.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaodi/osmapi/0a1a398014dc49b0fd51fe26e016332074bfaa64/tests/fixtures/test_ChangesetUpdate_wo_changeset.xml -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpload_create_node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpload_delete_relation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpload_invalid_response.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetUpload_modify_way.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/test_Changeset_create.xml: -------------------------------------------------------------------------------- 1 | 1414 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_Changeset_create_node.xml: -------------------------------------------------------------------------------- 1 | 7272 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_Changeset_upload.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/fixtures/test_ChangesetsGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeCreate.xml: -------------------------------------------------------------------------------- 1 | 9876 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeCreate_changesetauto.xml: -------------------------------------------------------------------------------- 1 | 7676 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeCreate_with_session_auth.xml: -------------------------------------------------------------------------------- 1 | 3322 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeCreate_wo_auth.xml: -------------------------------------------------------------------------------- 1 | 123 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeDelete.xml: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeGet_invalid_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeGet_with_version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeHistory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeRelations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeRelationsUnusedElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeUpdate.xml: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeUpdateConflict.xml: -------------------------------------------------------------------------------- 1 | Version does not match the current database version of the element 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeUpdateWhenChangesetIsClosed.xml: -------------------------------------------------------------------------------- 1 | The changeset 2222 was closed at 2021-11-20 09:42:47 UTC. 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeWays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodeWaysNotExists.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_NodesGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteAlreadyClosed.xml: -------------------------------------------------------------------------------- 1 | The note 819 was closed at 2022-04-29 20:57:20 UTC 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteClose.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 815 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/815 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/815/reopen 7 | 2014-10-03 15:20:57 UTC 8 | closed 9 | 2014-10-05 16:35:13 UTC 10 | 11 | 12 | 2014-10-03 15:20:57 UTC 13 | 1841 14 | metaodi 15 | http://master.apis.dev.openstreetmap.org/user/metaodi 16 | opened 17 | This is a test 18 | <p>This is a test</p> 19 | 20 | 21 | 2014-10-05 16:35:13 UTC 22 | 1841 23 | metaodi 24 | http://master.apis.dev.openstreetmap.org/user/metaodi 25 | closed 26 | Close this note! 27 | <p>Close this note!</p> 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteComment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 812 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/812 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/812/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/812/close 8 | 2014-10-03 15:11:05 UTC 9 | open 10 | 11 | 12 | 2014-10-03 15:11:05 UTC 13 | 1841 14 | metaodi 15 | http://master.apis.dev.openstreetmap.org/user/metaodi 16 | opened 17 | This is a test 18 | <p>This is a test</p> 19 | 20 | 21 | 2014-10-04 22:36:35 UTC 22 | 1841 23 | metaodi 24 | http://master.apis.dev.openstreetmap.org/user/metaodi 25 | commented 26 | This is a comment 27 | <p>This is a comment</p> 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteCommentAnonymous.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 842 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/842 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/842/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/842/close 8 | 2015-01-03 10:49:39 UTC 9 | open 10 | 11 | 12 | 2015-01-03 10:49:39 UTC 13 | opened 14 | test 123 15 | <p>test 123</p> 16 | 17 | 18 | 2015-01-03 11:06:00 UTC 19 | commented 20 | blubb 21 | <p>blubb</p> 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteCommentOnClosedNote.xml: -------------------------------------------------------------------------------- 1 | The note 817 was closed at 2022-04-29 20:57:20 UTC 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteCreate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 816 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/816 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/816/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/816/close 8 | 2014-10-03 15:21:21 UTC 9 | open 10 | 11 | 12 | 2014-10-03 15:21:22 UTC 13 | 1841 14 | metaodi 15 | http://master.apis.dev.openstreetmap.org/user/metaodi 16 | opened 17 | This is a test 18 | <p>This is a test</p> 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteCreateAnonymous.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 842 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/842 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/842/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/842/close 8 | 2015-01-03 10:49:39 UTC 9 | open 10 | 11 | 12 | 2015-01-03 10:49:39 UTC 13 | opened 14 | test 123 15 | <p>test 123</p> 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteCreate_wo_auth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 824 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/824 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/824/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/824/close 8 | 2014-10-03 16:09:11 UTC 9 | open 10 | 11 | 12 | 2014-10-03 16:09:12 UTC 13 | opened 14 | This is an unauthenticated test 15 | <p>This is an unauthenticated test</p> 16 | 17 | 18 | 19 | 20 | 21 | {'comments': [{'action': 'opened', 22 | 'date': '2014-10-03 16:09:12 UTC', 23 | 'html': '

This is an unauthenticated test

', 24 | 'text': 'This is an unauthenticated test', 25 | 'uid': None, 26 | 'user': None}], 27 | 'date_closed': None, 28 | 'date_created': '2014-10-03 16:09:11 UTC', 29 | 'id': '824', 30 | 'lat': 47.123, 31 | 'lon': 8.432, 32 | 'status': 'open'} 33 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1111 5 | http://api.openstreetmap.org/api/0.6/notes/1111 6 | http://api.openstreetmap.org/api/0.6/notes/1111/reopen 7 | 2013-05-01 20:58:21 UTC 8 | closed 9 | 2013-08-21 16:43:26 UTC 10 | 11 | 12 | 2013-05-01 20:58:21 UTC 13 | 1363438 14 | giuseppemari 15 | http://www.openstreetmap.org/user/giuseppemari 16 | opened 17 | It does not exist this path 18 | <p>It does not exist this path</p> 19 | 20 | 21 | 2013-08-21 16:43:26 UTC 22 | 1714220 23 | luschi 24 | http://www.openstreetmap.org/user/luschi 25 | closed 26 | there is no path signed 27 | <p>there is no path signed</p> 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteGet_invalid_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1111 5 | http://api.openstreetmap.org/api/0.6/notes/1111 6 | http://api.openstreetmap.org/api/0.6/notes/1111/reopen 7 | 2013-05-01 20:58:21 UTC 8 | closed 9 | 2013-08-21 16:43:26 UTC 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/test_NoteReopen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 815 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/815 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/815/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/815/close 8 | 2014-10-03 15:20:57 UTC 9 | open 10 | 11 | 12 | 2014-10-03 15:20:57 UTC 13 | 1841 14 | metaodi 15 | http://master.apis.dev.openstreetmap.org/user/metaodi 16 | opened 17 | This is a test 18 | <p>This is a test</p> 19 | 20 | 21 | 2014-10-05 16:35:13 UTC 22 | 1841 23 | metaodi 24 | http://master.apis.dev.openstreetmap.org/user/metaodi 25 | closed 26 | Close this note! 27 | <p>Close this note!</p> 28 | 29 | 30 | 2014-10-05 16:44:56 UTC 31 | 1841 32 | metaodi 33 | http://master.apis.dev.openstreetmap.org/user/metaodi 34 | reopened 35 | Reopen this note! 36 | <p>Reopen this note!</p> 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/fixtures/test_NotesGet_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_NotesSearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 796 5 | http://api06.dev.openstreetmap.org/api/0.6/notes/796 6 | http://api06.dev.openstreetmap.org/api/0.6/notes/796/comment 7 | http://api06.dev.openstreetmap.org/api/0.6/notes/796/close 8 | 2014-07-16 16:42:46 UTC 9 | open 10 | 11 | 12 | 2014-07-16 16:42:46 UTC 13 | 2132 14 | kalaset 15 | http://master.apis.dev.openstreetmap.org/user/kalaset 16 | opened 17 | One way street: 18 | jjhghkklll 19 | <p>One way street: 20 | <br />jjhghkklll</p> 21 | 22 | 23 | 24 | 25 | 788 26 | http://api06.dev.openstreetmap.org/api/0.6/notes/788 27 | http://api06.dev.openstreetmap.org/api/0.6/notes/788/comment 28 | http://api06.dev.openstreetmap.org/api/0.6/notes/788/close 29 | 2014-07-16 16:12:41 UTC 30 | open 31 | 32 | 33 | 2014-07-16 16:12:41 UTC 34 | opened 35 | One way street: 36 | comment 37 | <p>One way street: 38 | <br />comment</p> 39 | 40 | 41 | 42 | 43 | 738 44 | http://api06.dev.openstreetmap.org/api/0.6/notes/738 45 | http://api06.dev.openstreetmap.org/api/0.6/notes/738/comment 46 | http://api06.dev.openstreetmap.org/api/0.6/notes/738/close 47 | 2014-07-03 12:09:05 UTC 48 | open 49 | 50 | 51 | 2014-07-03 12:09:05 UTC 52 | 2132 53 | kalaset 54 | http://master.apis.dev.openstreetmap.org/user/kalaset 55 | opened 56 | One way street tyuui 57 | <p>One way street tyuui</p> 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationCreate.xml: -------------------------------------------------------------------------------- 1 | 8989 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationDelete.xml: -------------------------------------------------------------------------------- 1 | 43 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationFull.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationGet_with_version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationHistory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationRelations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationRelationsUnusedElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationUpdate.xml: -------------------------------------------------------------------------------- 1 | 42 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_RelationsGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayCreate.xml: -------------------------------------------------------------------------------- 1 | 5454 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayDelete.xml: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayFull.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayFull_invalid_response.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayGet_nodata.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaodi/osmapi/0a1a398014dc49b0fd51fe26e016332074bfaa64/tests/fixtures/test_WayGet_nodata.xml -------------------------------------------------------------------------------- /tests/fixtures/test_WayGet_with_version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayHistory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayRelations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayRelationsUnusedElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayUpdate.xml: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_WayUpdatePreconditionFailed.xml: -------------------------------------------------------------------------------- 1 | Way 876 requires the nodes with id in (11950), which either do not exist, or are not visible. 2 | -------------------------------------------------------------------------------- /tests/fixtures/test_WaysGet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/helper_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | import osmapi 3 | from unittest import mock 4 | import os 5 | 6 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 7 | 8 | 9 | class TestOsmApiHelper(osmapi_test.TestOsmApi): 10 | def setUp(self): 11 | super().setUp() 12 | self.setupMock() 13 | 14 | def setupMock(self, status=200): 15 | mock_response = mock.Mock() 16 | mock_response.status_code = status 17 | mock_response.reason = "test reason" 18 | mock_response.content = "test response" 19 | 20 | self.mock_session = mock.Mock() 21 | self.mock_session.request = mock.Mock(return_value=mock_response) 22 | self.mock_session.close = mock.Mock() 23 | self.mock_session.auth = ("testuser", "testpassword") 24 | 25 | self.api = osmapi.OsmApi( 26 | api=self.api_base, 27 | session=self.mock_session, 28 | username="testuser", 29 | password="testpassword", 30 | ) 31 | 32 | def test_passwordfile_only(self): 33 | path = os.path.join(__location__, "fixtures", "passwordfile.txt") 34 | my_api = osmapi.OsmApi(passwordfile=path) 35 | self.assertEqual("testosm", my_api._username) 36 | self.assertEqual("testpass", my_api._password) 37 | 38 | def test_passwordfile_with_user(self): 39 | path = os.path.join(__location__, "fixtures", "passwordfile.txt") 40 | my_api = osmapi.OsmApi(username="testuser", passwordfile=path) 41 | self.assertEqual("testuser", my_api._username) 42 | self.assertEqual("testuserpass", my_api._password) 43 | 44 | def test_passwordfile_with_colon(self): 45 | path = os.path.join(__location__, "fixtures", "passwordfile_colon.txt") 46 | my_api = osmapi.OsmApi(username="testuser", passwordfile=path) 47 | self.assertEqual("testuser", my_api._username) 48 | self.assertEqual("test:userpass", my_api._password) 49 | 50 | def test_close_call(self): 51 | self.api.close() 52 | self.assertEqual(self.api._session._session.close.call_count, 1) 53 | 54 | def test_close_context_manager(self): 55 | with osmapi.OsmApi() as my_api: 56 | my_api._session.close = mock.Mock() 57 | self.assertEqual(my_api._session.close.call_count, 1) 58 | 59 | def test_http_request_get(self): 60 | response = self.api._session._http_request("GET", "/api/0.6/test", False, None) 61 | self.mock_session.request.assert_called_with( 62 | "GET", self.api_base + "/api/0.6/test", data=None, timeout=30 63 | ) 64 | self.assertEqual(response, "test response") 65 | self.assertEqual(self.mock_session.request.call_count, 1) 66 | 67 | def test_http_request_put(self): 68 | data = "data" 69 | response = self.api._session._http_request( 70 | "PUT", "/api/0.6/testput", False, data 71 | ) 72 | self.mock_session.request.assert_called_with( 73 | "PUT", self.api_base + "/api/0.6/testput", data="data", timeout=30 74 | ) 75 | self.assertEqual(response, "test response") 76 | 77 | def test_http_request_delete(self): 78 | data = "delete data" 79 | response = self.api._session._http_request( 80 | "PUT", "/api/0.6/testdelete", False, data 81 | ) 82 | self.mock_session.request.assert_called_with( 83 | "PUT", self.api_base + "/api/0.6/testdelete", data="delete data", timeout=30 84 | ) 85 | self.assertEqual(response, "test response") 86 | 87 | def test_http_request_auth(self): 88 | response = self.api._session._http_request( 89 | "PUT", "/api/0.6/testauth", True, None 90 | ) 91 | self.mock_session.request.assert_called_with( 92 | "PUT", self.api_base + "/api/0.6/testauth", data=None, timeout=30 93 | ) 94 | self.assertEqual(self.mock_session.auth, ("testuser", "testpassword")) 95 | self.assertEqual(response, "test response") 96 | 97 | def test_http_request_410_response(self): 98 | self.setupMock(410) 99 | with self.assertRaises(osmapi.ElementDeletedApiError) as cm: 100 | self.api._session._http_request("GET", "/api/0.6/test410", False, None) 101 | self.assertEqual(cm.exception.status, 410) 102 | self.assertEqual(cm.exception.reason, "test reason") 103 | self.assertEqual(cm.exception.payload, "test response") 104 | 105 | def test_http_request_500_response(self): 106 | self.setupMock(500) 107 | with self.assertRaises(osmapi.ApiError) as cm: 108 | self.api._session._http_request( 109 | "GET", self.api_base + "/api/0.6/test500", False, None 110 | ) 111 | self.assertEqual(cm.exception.status, 500) 112 | self.assertEqual(cm.exception.reason, "test reason") 113 | self.assertEqual(cm.exception.payload, "test response") 114 | -------------------------------------------------------------------------------- /tests/node_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | import osmapi 3 | from unittest import mock 4 | import datetime 5 | from requests.auth import HTTPBasicAuth 6 | 7 | 8 | class TestOsmApiNode(osmapi_test.TestOsmApi): 9 | def test_NodeGet(self): 10 | self._session_mock() 11 | 12 | result = self.api.NodeGet(123) 13 | 14 | args, kwargs = self.session_mock.request.call_args 15 | self.assertEqual(args[0], "GET") 16 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/123") 17 | 18 | self.assertEqual( 19 | result, 20 | { 21 | "id": 123, 22 | "changeset": 15293, 23 | "uid": 605, 24 | "timestamp": datetime.datetime(2012, 4, 18, 11, 14, 26), 25 | "lat": 51.8753146, 26 | "lon": -1.4857118, 27 | "visible": True, 28 | "version": 8, 29 | "user": "freundchen", 30 | "tag": {"amenity": "school", "foo": "bar", "name": "Berolina & Schule"}, 31 | }, 32 | ) 33 | 34 | def test_NodeGet_with_version(self): 35 | self._session_mock() 36 | 37 | result = self.api.NodeGet(123, NodeVersion=2) 38 | 39 | args, kwargs = self.session_mock.request.call_args 40 | self.assertEqual(args[0], "GET") 41 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/123/2") 42 | 43 | self.assertEqual( 44 | result, 45 | { 46 | "id": 123, 47 | "changeset": 4152, 48 | "uid": 605, 49 | "timestamp": datetime.datetime(2011, 4, 18, 11, 14, 26), 50 | "lat": 51.8753146, 51 | "lon": -1.4857118, 52 | "visible": True, 53 | "version": 2, 54 | "user": "freundchen", 55 | "tag": { 56 | "amenity": "school", 57 | }, 58 | }, 59 | ) 60 | 61 | def test_NodeGet_invalid_response(self): 62 | self._session_mock() 63 | 64 | with self.assertRaises(osmapi.XmlResponseInvalidError): 65 | self.api.NodeGet(987) 66 | 67 | def test_NodeCreate_changesetauto(self): 68 | for filename in [ 69 | "test_NodeCreate_changesetauto.xml", 70 | "test_ChangesetUpload_create_node.xml", 71 | "test_ChangesetClose.xml", 72 | ]: 73 | # setup mock 74 | self._session_mock(auth=True, filenames=[filename]) 75 | self.api = osmapi.OsmApi( 76 | api="api06.dev.openstreetmap.org", 77 | changesetauto=True, 78 | session=self.session_mock, 79 | ) 80 | self.api._session._sleep = mock.Mock() 81 | 82 | test_node = { 83 | "lat": 47.123, 84 | "lon": 8.555, 85 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 86 | } 87 | 88 | self.assertIsNone(self.api.NodeCreate(test_node)) 89 | 90 | def test_NodeCreate(self): 91 | self._session_mock(auth=True) 92 | 93 | # setup mock 94 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 95 | self.api._CurrentChangesetId = 1111 96 | 97 | test_node = { 98 | "lat": 47.287, 99 | "lon": 8.765, 100 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 101 | } 102 | 103 | cs = self.api.ChangesetCreate({"comment": "This is a test dataset"}) 104 | self.assertEqual(cs, 1111) 105 | result = self.api.NodeCreate(test_node) 106 | 107 | args, kwargs = self.session_mock.request.call_args 108 | self.assertEqual(args[0], "PUT") 109 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/create") 110 | 111 | self.assertEqual(result["id"], 9876) 112 | self.assertEqual(result["lat"], test_node["lat"]) 113 | self.assertEqual(result["lon"], test_node["lon"]) 114 | self.assertEqual(result["tag"], test_node["tag"]) 115 | 116 | def test_NodeCreate_wo_changeset(self): 117 | test_node = { 118 | "lat": 47.287, 119 | "lon": 8.765, 120 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 121 | } 122 | 123 | with self.assertRaisesRegex( 124 | osmapi.NoChangesetOpenError, "need to open a changeset" 125 | ): 126 | self.api.NodeCreate(test_node) 127 | 128 | def test_NodeCreate_existing_node(self): 129 | # setup mock 130 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 131 | self.api._CurrentChangesetId = 1111 132 | 133 | test_node = { 134 | "id": 123, 135 | "lat": 47.287, 136 | "lon": 8.765, 137 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 138 | } 139 | 140 | with self.assertRaisesRegex( 141 | osmapi.OsmTypeAlreadyExistsError, "This node already exists" 142 | ): 143 | self.api.NodeCreate(test_node) 144 | 145 | def test_NodeCreate_wo_auth(self): 146 | self._session_mock() 147 | 148 | # setup mock 149 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 150 | self.api._CurrentChangesetId = 1111 151 | test_node = { 152 | "lat": 47.287, 153 | "lon": 8.765, 154 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 155 | } 156 | 157 | with self.assertRaisesRegex( 158 | osmapi.UsernamePasswordMissingError, "Username/Password missing" 159 | ): 160 | self.api.NodeCreate(test_node) 161 | 162 | def test_NodeCreate_unauthorized(self): 163 | self._session_mock(auth=True, status=401) 164 | 165 | # setup mock 166 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 167 | self.api._CurrentChangesetId = 1111 168 | test_node = { 169 | "lat": 47.287, 170 | "lon": 8.765, 171 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 172 | } 173 | 174 | with self.assertRaises(osmapi.UnauthorizedApiError): 175 | self.api.NodeCreate(test_node) 176 | 177 | def test_NodeCreate_with_session_auth(self): 178 | self._session_mock() 179 | self.session_mock.auth = HTTPBasicAuth("user", "pass") 180 | 181 | api = osmapi.OsmApi(api=self.api_base, session=self.session_mock) 182 | 183 | # setup mock 184 | api.ChangesetCreate = mock.Mock(return_value=1111) 185 | api._CurrentChangesetId = 1111 186 | test_node = { 187 | "lat": 47.287, 188 | "lon": 8.765, 189 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 190 | } 191 | 192 | cs = api.ChangesetCreate({"comment": "This is a test dataset"}) 193 | self.assertEqual(cs, 1111) 194 | result = api.NodeCreate(test_node) 195 | self.assertEqual(result["id"], 3322) 196 | 197 | def test_NodeCreate_with_exception(self): 198 | self._session_mock(auth=True) 199 | self.api._session._http_request = mock.Mock(side_effect=Exception) 200 | 201 | # setup mock 202 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 203 | self.api._CurrentChangesetId = 1111 204 | test_node = { 205 | "lat": 47.287, 206 | "lon": 8.765, 207 | "tag": {"amenity": "place_of_worship", "religion": "pastafarian"}, 208 | } 209 | 210 | with self.assertRaisesRegex( 211 | osmapi.MaximumRetryLimitReachedError, "Give up after 5 retries" 212 | ): 213 | self.api.NodeCreate(test_node) 214 | 215 | def test_NodeUpdate(self): 216 | self._session_mock(auth=True) 217 | 218 | # setup mock 219 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 220 | self.api._CurrentChangesetId = 1111 221 | 222 | test_node = { 223 | "id": 7676, 224 | "lat": 47.287, 225 | "lon": 8.765, 226 | "tag": {"amenity": "place_of_worship", "name": "christian"}, 227 | } 228 | 229 | cs = self.api.ChangesetCreate({"comment": "This is a test dataset"}) 230 | self.assertEqual(cs, 1111) 231 | result = self.api.NodeUpdate(test_node) 232 | 233 | args, kwargs = self.session_mock.request.call_args 234 | self.assertEqual(args[0], "PUT") 235 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/7676") 236 | 237 | self.assertEqual(result["id"], 7676) 238 | self.assertEqual(result["version"], 3) 239 | self.assertEqual(result["lat"], test_node["lat"]) 240 | self.assertEqual(result["lon"], test_node["lon"]) 241 | self.assertEqual(result["tag"], test_node["tag"]) 242 | 243 | def test_NodeUpdateWhenChangesetIsClosed(self): 244 | self._session_mock(auth=True, status=409) 245 | 246 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 247 | self.api._CurrentChangesetId = 1111 248 | 249 | test_node = { 250 | "id": 7676, 251 | "lat": 47.287, 252 | "lon": 8.765, 253 | "tag": {"amenity": "place_of_worship", "name": "christian"}, 254 | } 255 | 256 | self.api.ChangesetCreate({"comment": "This is a test dataset"}) 257 | 258 | with self.assertRaises(osmapi.ChangesetClosedApiError) as cm: 259 | self.api.NodeUpdate(test_node) 260 | 261 | self.assertEqual(cm.exception.status, 409) 262 | self.assertEqual( 263 | cm.exception.payload, 264 | "The changeset 2222 was closed at 2021-11-20 09:42:47 UTC.", 265 | ) 266 | 267 | def test_NodeUpdateConflict(self): 268 | self._session_mock(auth=True, status=409) 269 | 270 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 271 | self.api._CurrentChangesetId = 1111 272 | 273 | test_node = { 274 | "id": 7676, 275 | "lat": 47.287, 276 | "lon": 8.765, 277 | "tag": {"amenity": "place_of_worship", "name": "christian"}, 278 | } 279 | 280 | self.api.ChangesetCreate({"comment": "This is a test dataset"}) 281 | 282 | with self.assertRaises(osmapi.VersionMismatchApiError) as cm: 283 | self.api.NodeUpdate(test_node) 284 | 285 | self.assertEqual(cm.exception.status, 409) 286 | self.assertEqual( 287 | cm.exception.payload, 288 | "Version does not match the current database version of the element", 289 | ) 290 | 291 | def test_NodeDelete(self): 292 | self._session_mock(auth=True) 293 | 294 | # setup mock 295 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 296 | self.api._CurrentChangesetId = 1111 297 | 298 | test_node = {"id": 7676} 299 | 300 | cs = self.api.ChangesetCreate({"comment": "This is a test dataset"}) 301 | self.assertEqual(cs, 1111) 302 | 303 | result = self.api.NodeDelete(test_node) 304 | 305 | args, kwargs = self.session_mock.request.call_args 306 | self.assertEqual(args[0], "DELETE") 307 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/7676") 308 | self.assertEqual(result["id"], 7676) 309 | self.assertEqual(result["version"], 4) 310 | 311 | def test_NodeHistory(self): 312 | self._session_mock() 313 | 314 | result = self.api.NodeHistory(123) 315 | 316 | args, kwargs = self.session_mock.request.call_args 317 | self.assertEqual(args[0], "GET") 318 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/123/history") 319 | 320 | self.assertEqual(len(result), 8) 321 | self.assertEqual(result[4]["id"], 123) 322 | self.assertEqual(result[4]["version"], 4) 323 | self.assertEqual(result[4]["lat"], 51.8753146) 324 | self.assertEqual(result[4]["lon"], -1.4857118) 325 | self.assertEqual( 326 | result[4]["tag"], 327 | { 328 | "empty": "", 329 | "foo": "bar", 330 | }, 331 | ) 332 | 333 | def test_NodeWays(self): 334 | self._session_mock() 335 | 336 | result = self.api.NodeWays(234) 337 | 338 | args, kwargs = self.session_mock.request.call_args 339 | self.assertEqual(args[0], "GET") 340 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/234/ways") 341 | 342 | self.assertEqual(len(result), 1) 343 | self.assertEqual(result[0]["id"], 60) 344 | self.assertEqual(result[0]["changeset"], 61) 345 | self.assertEqual( 346 | result[0]["tag"], 347 | { 348 | "highway": "path", 349 | "name": "Dog walking path", 350 | }, 351 | ) 352 | 353 | def test_NodeWaysNotExists(self): 354 | self._session_mock() 355 | 356 | result = self.api.NodeWays(404) 357 | 358 | args, kwargs = self.session_mock.request.call_args 359 | self.assertEqual(args[0], "GET") 360 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/node/404/ways") 361 | 362 | self.assertEqual(len(result), 0) 363 | self.assertIsInstance(result, list) 364 | 365 | def test_NodeRelations(self): 366 | self._session_mock() 367 | 368 | result = self.api.NodeRelations(4295668179) 369 | 370 | args, kwargs = self.session_mock.request.call_args 371 | self.assertEqual(args[0], "GET") 372 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/node/4295668179/relations") 373 | 374 | self.assertEqual(len(result), 1) 375 | self.assertEqual(result[0]["id"], 4294968148) 376 | self.assertEqual(result[0]["changeset"], 23123) 377 | self.assertEqual( 378 | result[0]["member"][1], 379 | { 380 | "role": "point", 381 | "ref": 4295668179, 382 | "type": "node", 383 | }, 384 | ) 385 | self.assertEqual( 386 | result[0]["tag"], 387 | { 388 | "type": "fancy", 389 | }, 390 | ) 391 | 392 | def test_NodeRelationsUnusedElement(self): 393 | self._session_mock() 394 | 395 | result = self.api.NodeRelations(4295668179) 396 | 397 | args, kwargs = self.session_mock.request.call_args 398 | self.assertEqual(args[0], "GET") 399 | self.assertEqual(args[1], self.api_base + "/api/0.6/node/4295668179/relations") 400 | 401 | self.assertEqual(len(result), 0) 402 | self.assertIsInstance(result, list) 403 | 404 | def test_NodesGet(self): 405 | self._session_mock() 406 | 407 | result = self.api.NodesGet([123, 345]) 408 | 409 | args, kwargs = self.session_mock.request.call_args 410 | self.assertEqual(args[0], "GET") 411 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/nodes?nodes=123,345") 412 | 413 | self.assertEqual(len(result), 2) 414 | self.assertEqual( 415 | result[123], 416 | { 417 | "id": 123, 418 | "changeset": 15293, 419 | "uid": 605, 420 | "timestamp": datetime.datetime(2012, 4, 18, 11, 14, 26), 421 | "lat": 51.8753146, 422 | "lon": -1.4857118, 423 | "visible": True, 424 | "version": 8, 425 | "user": "freundchen", 426 | "tag": {"amenity": "school", "foo": "bar", "name": "Berolina & Schule"}, 427 | }, 428 | ) 429 | self.assertEqual( 430 | result[345], 431 | { 432 | "id": 345, 433 | "changeset": 244, 434 | "timestamp": datetime.datetime(2009, 9, 12, 3, 22, 59), 435 | "uid": 1, 436 | "visible": False, 437 | "version": 2, 438 | "user": "guggis", 439 | "tag": {}, 440 | }, 441 | ) 442 | -------------------------------------------------------------------------------- /tests/notes_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | from datetime import datetime 3 | import osmapi 4 | from urllib import parse as urlparse 5 | 6 | 7 | class TestOsmApiNotes(osmapi_test.TestOsmApi): 8 | def test_NotesGet(self): 9 | self._session_mock() 10 | 11 | result = self.api.NotesGet(-1.4998534, 45.9667901, -1.4831815, 52.4710193) 12 | 13 | args, kwargs = self.session_mock.request.call_args 14 | self.assertEqual(args[0], "GET") 15 | urlParts = urlparse.urlparse(args[1]) 16 | params = urlparse.parse_qs(urlParts.query) 17 | self.assertEqual(params["bbox"][0], "-1.499853,45.966790,-1.483181,52.471019") 18 | self.assertEqual(params["limit"][0], "100") 19 | self.assertEqual(params["closed"][0], "7") 20 | 21 | self.assertEqual(len(result), 14) 22 | self.assertEqual( 23 | result[2], 24 | { 25 | "id": "231775", 26 | "lon": -1.4929605, 27 | "lat": 52.4107312, 28 | "date_created": datetime(2014, 8, 28, 19, 25, 37), 29 | "date_closed": datetime(2014, 9, 27, 9, 21, 41), 30 | "status": "closed", 31 | "comments": [ 32 | { 33 | "date": datetime(2014, 8, 28, 19, 25, 37), 34 | "action": "opened", 35 | "text": "Is it Paynes or Payne's", 36 | "html": "

Is it Paynes or Payne's

", 37 | "uid": "1486336", 38 | "user": "Wyken Seagrave", 39 | }, 40 | { 41 | "date": datetime(2014, 9, 26, 13, 5, 33), 42 | "action": "commented", 43 | "text": "Royal Mail's postcode finder has PAYNES LANE", 44 | "html": ( 45 | "

Royal Mail's postcode finder " "has PAYNES LANE

" 46 | ), 47 | "uid": None, 48 | "user": None, 49 | }, 50 | ], 51 | }, 52 | ) 53 | 54 | def test_NotesGet_empty(self): 55 | self._session_mock() 56 | 57 | result = self.api.NotesGet( 58 | -93.8472901, 35.9763601, -80, 36.176360100000004, limit=1, closed=0 59 | ) 60 | 61 | args, kwargs = self.session_mock.request.call_args 62 | self.assertEqual(args[0], "GET") 63 | urlParts = urlparse.urlparse(args[1]) 64 | params = urlparse.parse_qs(urlParts.query) 65 | 66 | self.assertEqual(params["limit"][0], "1") 67 | self.assertEqual(params["closed"][0], "0") 68 | 69 | self.assertEqual(len(result), 0) 70 | self.assertEqual(result, []) 71 | 72 | def test_NoteGet(self): 73 | self._session_mock() 74 | 75 | result = self.api.NoteGet(1111) 76 | 77 | args, kwargs = self.session_mock.request.call_args 78 | self.assertEqual(args[0], "GET") 79 | self.assertEqual(args[1], self.api_base + "/api/0.6/notes/1111") 80 | 81 | self.assertEqual( 82 | result, 83 | { 84 | "id": "1111", 85 | "lon": 12.3133135, 86 | "lat": 37.9305489, 87 | "date_created": datetime(2013, 5, 1, 20, 58, 21), 88 | "date_closed": datetime(2013, 8, 21, 16, 43, 26), 89 | "status": "closed", 90 | "comments": [ 91 | { 92 | "date": datetime(2013, 5, 1, 20, 58, 21), 93 | "action": "opened", 94 | "text": "It does not exist this path", 95 | "html": "

It does not exist this path

", 96 | "uid": "1363438", 97 | "user": "giuseppemari", 98 | }, 99 | { 100 | "date": datetime(2013, 8, 21, 16, 43, 26), 101 | "action": "closed", 102 | "text": "there is no path signed", 103 | "html": "

there is no path signed

", 104 | "uid": "1714220", 105 | "user": "luschi", 106 | }, 107 | ], 108 | }, 109 | ) 110 | 111 | def test_NoteGet_invalid_xml(self): 112 | self._session_mock() 113 | 114 | with self.assertRaises(osmapi.XmlResponseInvalidError): 115 | self.api.NoteGet(1111) 116 | 117 | def test_NoteCreate(self): 118 | self._session_mock(auth=True) 119 | 120 | note = {"lat": 47.123, "lon": 8.432, "text": "This is a test"} 121 | result = self.api.NoteCreate(note) 122 | 123 | args, kwargs = self.session_mock.request.call_args 124 | self.assertEqual(args[0], "POST") 125 | urlParts = urlparse.urlparse(args[1]) 126 | params = urlparse.parse_qs(urlParts.query) 127 | self.assertEqual(params["lat"][0], "47.123") 128 | self.assertEqual(params["lon"][0], "8.432") 129 | self.assertEqual(params["text"][0], "This is a test") 130 | 131 | self.assertEqual( 132 | result, 133 | { 134 | "id": "816", 135 | "lat": 47.123, 136 | "lon": 8.432, 137 | "date_created": datetime(2014, 10, 3, 15, 21, 21), 138 | "date_closed": None, 139 | "status": "open", 140 | "comments": [ 141 | { 142 | "date": datetime(2014, 10, 3, 15, 21, 22), 143 | "action": "opened", 144 | "text": "This is a test", 145 | "html": "

This is a test

", 146 | "uid": "1841", 147 | "user": "metaodi", 148 | } 149 | ], 150 | }, 151 | ) 152 | 153 | def test_NoteCreateAnonymous(self): 154 | self._session_mock() 155 | 156 | note = {"lat": 47.123, "lon": 8.432, "text": "test 123"} 157 | result = self.api.NoteCreate(note) 158 | 159 | args, kwargs = self.session_mock.request.call_args 160 | self.assertEqual(args[0], "POST") 161 | urlParts = urlparse.urlparse(args[1]) 162 | params = urlparse.parse_qs(urlParts.query) 163 | self.assertEqual(params["lat"][0], "47.123") 164 | self.assertEqual(params["lon"][0], "8.432") 165 | self.assertEqual(params["text"][0], "test 123") 166 | 167 | self.assertEqual( 168 | result, 169 | { 170 | "id": "842", 171 | "lat": 58.3368222, 172 | "lon": 25.8826183, 173 | "date_created": datetime(2015, 1, 3, 10, 49, 39), 174 | "date_closed": None, 175 | "status": "open", 176 | "comments": [ 177 | { 178 | "date": datetime(2015, 1, 3, 10, 49, 39), 179 | "action": "opened", 180 | "text": "test 123", 181 | "html": "

test 123

", 182 | "uid": None, 183 | "user": None, 184 | } 185 | ], 186 | }, 187 | ) 188 | 189 | def test_NoteComment(self): 190 | self._session_mock(auth=True) 191 | 192 | result = self.api.NoteComment(812, "This is a comment") 193 | 194 | args, kwargs = self.session_mock.request.call_args 195 | self.assertEqual(args[0], "POST") 196 | self.assertEqual( 197 | args[1], self.api_base + "/api/0.6/notes/812/comment?text=This+is+a+comment" 198 | ) 199 | 200 | self.assertEqual( 201 | result, 202 | { 203 | "id": "812", 204 | "lat": 47.123, 205 | "lon": 8.432, 206 | "date_created": datetime(2014, 10, 3, 15, 11, 5), 207 | "date_closed": None, 208 | "status": "open", 209 | "comments": [ 210 | { 211 | "date": datetime(2014, 10, 3, 15, 11, 5), 212 | "action": "opened", 213 | "text": "This is a test", 214 | "html": "

This is a test

", 215 | "uid": "1841", 216 | "user": "metaodi", 217 | }, 218 | { 219 | "date": datetime(2014, 10, 4, 22, 36, 35), 220 | "action": "commented", 221 | "text": "This is a comment", 222 | "html": "

This is a comment

", 223 | "uid": "1841", 224 | "user": "metaodi", 225 | }, 226 | ], 227 | }, 228 | ) 229 | 230 | def test_NoteCommentAnonymous(self): 231 | self._session_mock() 232 | 233 | result = self.api.NoteComment(842, "blubb") 234 | 235 | args, kwargs = self.session_mock.request.call_args 236 | self.assertEqual(args[0], "POST") 237 | self.assertEqual( 238 | args[1], self.api_base + "/api/0.6/notes/842/comment?text=blubb" 239 | ) 240 | 241 | self.assertEqual( 242 | result, 243 | { 244 | "id": "842", 245 | "lat": 58.3368222, 246 | "lon": 25.8826183, 247 | "date_created": datetime(2015, 1, 3, 10, 49, 39), 248 | "date_closed": None, 249 | "status": "open", 250 | "comments": [ 251 | { 252 | "date": datetime(2015, 1, 3, 10, 49, 39), 253 | "action": "opened", 254 | "text": "test 123", 255 | "html": "

test 123

", 256 | "uid": None, 257 | "user": None, 258 | }, 259 | { 260 | "date": datetime(2015, 1, 3, 11, 6, 0), 261 | "action": "commented", 262 | "text": "blubb", 263 | "html": "

blubb

", 264 | "uid": None, 265 | "user": None, 266 | }, 267 | ], 268 | }, 269 | ) 270 | 271 | def test_NoteCommentOnClosedNote(self): 272 | self._session_mock(status=409) 273 | 274 | with self.assertRaises(osmapi.NoteAlreadyClosedApiError) as cm: 275 | self.api.NoteComment(817, "Comment on closed note") 276 | 277 | self.assertEqual(cm.exception.status, 409) 278 | self.assertEqual( 279 | cm.exception.payload, "The note 817 was closed at 2022-04-29 20:57:20 UTC" 280 | ) 281 | 282 | def test_NoteComment_non_existing_note(self): 283 | self._session_mock(status=404) 284 | 285 | with self.assertRaises(osmapi.ElementNotFoundApiError) as cm: 286 | self.api.NoteComment(817, "Comment on closed note") 287 | 288 | self.assertEqual(cm.exception.status, 404) 289 | 290 | def test_NoteClose(self): 291 | self._session_mock(auth=True) 292 | 293 | result = self.api.NoteClose(819, "Close this note!") 294 | 295 | args, kwargs = self.session_mock.request.call_args 296 | self.assertEqual(args[0], "POST") 297 | self.assertEqual( 298 | args[1], self.api_base + "/api/0.6/notes/819/close?text=Close+this+note%21" 299 | ) 300 | 301 | self.assertEqual( 302 | result, 303 | { 304 | "id": "815", 305 | "lat": 47.123, 306 | "lon": 8.432, 307 | "date_created": datetime(2014, 10, 3, 15, 20, 57), 308 | "date_closed": datetime(2014, 10, 5, 16, 35, 13), 309 | "status": "closed", 310 | "comments": [ 311 | { 312 | "date": datetime(2014, 10, 3, 15, 20, 57), 313 | "action": "opened", 314 | "text": "This is a test", 315 | "html": "

This is a test

", 316 | "uid": "1841", 317 | "user": "metaodi", 318 | }, 319 | { 320 | "date": datetime(2014, 10, 5, 16, 35, 13), 321 | "action": "closed", 322 | "text": "Close this note!", 323 | "html": "

Close this note!

", 324 | "uid": "1841", 325 | "user": "metaodi", 326 | }, 327 | ], 328 | }, 329 | ) 330 | 331 | def test_NoteAlreadyClosed(self): 332 | self._session_mock(auth=True, status=409) 333 | 334 | with self.assertRaises(osmapi.NoteAlreadyClosedApiError) as cm: 335 | self.api.NoteClose(819, "Close this note!") 336 | 337 | self.assertEqual(cm.exception.status, 409) 338 | self.assertEqual( 339 | cm.exception.payload, "The note 819 was closed at 2022-04-29 20:57:20 UTC" 340 | ) 341 | 342 | def test_NoteReopen(self): 343 | self._session_mock(auth=True) 344 | 345 | result = self.api.NoteReopen(815, "Reopen this note!") 346 | 347 | args, kwargs = self.session_mock.request.call_args 348 | self.assertEqual(args[0], "POST") 349 | self.assertEqual( 350 | args[1], 351 | (self.api_base + "/api/0.6/notes/815/reopen?text=Reopen+this+note%21"), 352 | ) 353 | 354 | self.assertEqual( 355 | result, 356 | { 357 | "id": "815", 358 | "lat": 47.123, 359 | "lon": 8.432, 360 | "date_created": datetime(2014, 10, 3, 15, 20, 57), 361 | "date_closed": None, 362 | "status": "open", 363 | "comments": [ 364 | { 365 | "date": datetime(2014, 10, 3, 15, 20, 57), 366 | "action": "opened", 367 | "text": "This is a test", 368 | "html": "

This is a test

", 369 | "uid": "1841", 370 | "user": "metaodi", 371 | }, 372 | { 373 | "date": datetime(2014, 10, 5, 16, 35, 13), 374 | "action": "closed", 375 | "text": "Close this note!", 376 | "html": "

Close this note!

", 377 | "uid": "1841", 378 | "user": "metaodi", 379 | }, 380 | { 381 | "date": datetime(2014, 10, 5, 16, 44, 56), 382 | "action": "reopened", 383 | "text": "Reopen this note!", 384 | "html": "

Reopen this note!

", 385 | "uid": "1841", 386 | "user": "metaodi", 387 | }, 388 | ], 389 | }, 390 | ) 391 | 392 | def test_NotesSearch(self): 393 | self._session_mock() 394 | 395 | result = self.api.NotesSearch("street") 396 | 397 | args, kwargs = self.session_mock.request.call_args 398 | self.assertEqual(args[0], "GET") 399 | urlParts = urlparse.urlparse(args[1]) 400 | params = urlparse.parse_qs(urlParts.query) 401 | self.assertEqual(params["q"][0], "street") 402 | self.assertEqual(params["limit"][0], "100") 403 | self.assertEqual(params["closed"][0], "7") 404 | 405 | self.assertEqual(len(result), 3) 406 | self.assertEqual( 407 | result[1], 408 | { 409 | "id": "788", 410 | "lon": 11.96395, 411 | "lat": 57.70301, 412 | "date_created": datetime(2014, 7, 16, 16, 12, 41), 413 | "date_closed": None, 414 | "status": "open", 415 | "comments": [ 416 | { 417 | "date": datetime(2014, 7, 16, 16, 12, 41), 418 | "action": "opened", 419 | "text": "One way street:\ncomment", 420 | "html": "

One way street:\n
comment

", 421 | "uid": None, 422 | "user": None, 423 | } 424 | ], 425 | }, 426 | ) 427 | -------------------------------------------------------------------------------- /tests/osmapi_test.py: -------------------------------------------------------------------------------- 1 | from osmapi import OsmApi 2 | from unittest import mock 3 | import os 4 | import unittest 5 | import codecs 6 | 7 | __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) 8 | 9 | 10 | class TestOsmApi(unittest.TestCase): 11 | def setUp(self): 12 | self.api_base = "http://api06.dev.openstreetmap.org" 13 | self.api = OsmApi(api=self.api_base) 14 | self.maxDiff = None 15 | print(self._testMethodName) 16 | print(self.api) 17 | 18 | def _session_mock(self, auth=False, filenames=None, status=200): 19 | response_mock = mock.Mock() 20 | response_mock.status_code = status 21 | return_values = self._return_values(filenames) 22 | print(filenames) 23 | print(return_values) 24 | assert len(return_values) < 2 25 | if return_values: 26 | response_mock.content = return_values[0] 27 | 28 | self.session_mock = mock.Mock() 29 | self.session_mock.request = mock.Mock(return_value=response_mock) 30 | self.session_mock.auth = None 31 | 32 | if auth: 33 | self.api = OsmApi( 34 | api=self.api_base, 35 | username="testuser", 36 | password="testpassword", 37 | session=self.session_mock, 38 | ) 39 | else: 40 | self.api = OsmApi(api=self.api_base, session=self.session_mock) 41 | 42 | self.api._get_http_session = mock.Mock(return_value=self.session_mock) 43 | self.api._session._sleep = mock.Mock() 44 | 45 | def _return_values(self, filenames): 46 | if filenames is None: 47 | filenames = [self._testMethodName + ".xml"] 48 | 49 | return_values = [] 50 | for filename in filenames: 51 | path = os.path.join(__location__, "fixtures", filename) 52 | try: 53 | with codecs.open(path, "r", "utf-8") as file: 54 | return_values.append(file.read()) 55 | except Exception: 56 | pass 57 | return return_values 58 | 59 | def teardown(self): 60 | pass 61 | 62 | def test_constructor(self): 63 | self.assertTrue(isinstance(self.api, OsmApi)) 64 | -------------------------------------------------------------------------------- /tests/relation_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | import osmapi 3 | from unittest import mock 4 | import datetime 5 | 6 | 7 | class TestOsmApiRelation(osmapi_test.TestOsmApi): 8 | def test_RelationGet(self): 9 | self._session_mock() 10 | 11 | result = self.api.RelationGet(321) 12 | 13 | args, kwargs = self.session_mock.request.call_args 14 | self.assertEqual(args[0], "GET") 15 | self.assertEqual(args[1], self.api_base + "/api/0.6/relation/321") 16 | 17 | self.assertEqual( 18 | result, 19 | { 20 | "id": 321, 21 | "changeset": 434, 22 | "uid": 12, 23 | "timestamp": datetime.datetime(2009, 9, 15, 22, 24, 25), 24 | "visible": True, 25 | "version": 1, 26 | "user": "green525", 27 | "tag": { 28 | "admin_level": "9", 29 | "boundary": "administrative", 30 | "type": "multipolygon", 31 | }, 32 | "member": [ 33 | {"ref": 6908, "role": "outer", "type": "way"}, 34 | {"ref": 6352, "role": "outer", "type": "way"}, 35 | {"ref": 5669, "role": "outer", "type": "way"}, 36 | {"ref": 5682, "role": "outer", "type": "way"}, 37 | {"ref": 6909, "role": "outer", "type": "way"}, 38 | {"ref": 6355, "role": "outer", "type": "way"}, 39 | {"ref": 6910, "role": "outer", "type": "way"}, 40 | {"ref": 6911, "role": "outer", "type": "way"}, 41 | {"ref": 6912, "role": "outer", "type": "way"}, 42 | ], 43 | }, 44 | ) 45 | 46 | def test_RelationGet_with_version(self): 47 | self._session_mock() 48 | 49 | result = self.api.RelationGet(765, 2) 50 | 51 | args, kwargs = self.session_mock.request.call_args 52 | self.assertEqual(args[0], "GET") 53 | self.assertEqual(args[1], self.api_base + "/api/0.6/relation/765/2") 54 | 55 | self.assertEqual(result["id"], 765) 56 | self.assertEqual(result["changeset"], 41378) 57 | self.assertEqual(result["user"], "metaodi") 58 | self.assertEqual(result["tag"]["source"], "test") 59 | 60 | def test_RelationCreate(self): 61 | self._session_mock(auth=True) 62 | 63 | # setup mock 64 | self.api.ChangesetCreate = mock.Mock(return_value=3333) 65 | self.api._CurrentChangesetId = 3333 66 | 67 | test_relation = { 68 | "tag": { 69 | "type": "test", 70 | }, 71 | "member": [ 72 | {"ref": 6908, "role": "outer", "type": "way"}, 73 | {"ref": 6352, "role": "point", "type": "node"}, 74 | ], 75 | } 76 | 77 | cs = self.api.ChangesetCreate({"comment": "This is a test relation"}) 78 | self.assertEqual(cs, 3333) 79 | 80 | result = self.api.RelationCreate(test_relation) 81 | 82 | args, kwargs = self.session_mock.request.call_args 83 | self.assertEqual(args[0], "PUT") 84 | self.assertEqual(args[1], self.api_base + "/api/0.6/relation/create") 85 | 86 | self.assertEqual(result["id"], 8989) 87 | self.assertEqual(result["version"], 1) 88 | self.assertEqual(result["member"], test_relation["member"]) 89 | self.assertEqual(result["tag"], test_relation["tag"]) 90 | 91 | def test_RelationCreate_existing_node(self): 92 | # setup mock 93 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 94 | self.api._CurrentChangesetId = 1111 95 | 96 | test_relation = { 97 | "id": 456, 98 | "tag": { 99 | "type": "test", 100 | }, 101 | "member": [ 102 | {"ref": 6908, "role": "outer", "type": "way"}, 103 | {"ref": 6352, "role": "point", "type": "node"}, 104 | ], 105 | } 106 | 107 | with self.assertRaisesRegex( 108 | osmapi.OsmTypeAlreadyExistsError, "This relation already exists" 109 | ): 110 | self.api.RelationCreate(test_relation) 111 | 112 | def test_RelationUpdate(self): 113 | self._session_mock(auth=True) 114 | 115 | # setup mock 116 | self.api.ChangesetCreate = mock.Mock(return_value=3333) 117 | self.api._CurrentChangesetId = 3333 118 | 119 | test_relation = { 120 | "id": 8989, 121 | "tag": { 122 | "type": "test update", 123 | }, 124 | "member": [{"ref": 6908, "role": "outer", "type": "way"}], 125 | } 126 | 127 | cs = self.api.ChangesetCreate({"comment": "This is a test relation"}) 128 | self.assertEqual(cs, 3333) 129 | 130 | result = self.api.RelationUpdate(test_relation) 131 | 132 | args, kwargs = self.session_mock.request.call_args 133 | self.assertEqual(args[0], "PUT") 134 | self.assertEqual(args[1], self.api_base + "/api/0.6/relation/8989") 135 | 136 | self.assertEqual(result["id"], 8989) 137 | self.assertEqual(result["version"], 42) 138 | self.assertEqual(result["member"], test_relation["member"]) 139 | self.assertEqual(result["tag"], test_relation["tag"]) 140 | 141 | def test_RelationDelete(self): 142 | self._session_mock(auth=True) 143 | 144 | # setup mock 145 | self.api.ChangesetCreate = mock.Mock(return_value=3333) 146 | self.api._CurrentChangesetId = 3333 147 | 148 | test_relation = {"id": 8989} 149 | 150 | cs = self.api.ChangesetCreate({"comment": "This is a test relation"}) 151 | self.assertEqual(cs, 3333) 152 | 153 | result = self.api.RelationDelete(test_relation) 154 | 155 | args, kwargs = self.session_mock.request.call_args 156 | self.assertEqual(args[0], "DELETE") 157 | self.assertEqual(args[1], self.api_base + "/api/0.6/relation/8989") 158 | 159 | self.assertEqual(result["id"], 8989) 160 | self.assertEqual(result["version"], 43) 161 | 162 | def test_RelationHistory(self): 163 | self._session_mock() 164 | 165 | result = self.api.RelationHistory(2470397) 166 | 167 | args, kwargs = self.session_mock.request.call_args 168 | self.assertEqual(args[0], "GET") 169 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/relation/2470397/history") 170 | 171 | self.assertEqual(len(result), 2) 172 | self.assertEqual(result[1]["id"], 2470397) 173 | self.assertEqual(result[1]["version"], 1) 174 | self.assertEqual( 175 | result[1]["tag"], 176 | { 177 | "restriction": "only_straight_on", 178 | "type": "restriction", 179 | }, 180 | ) 181 | self.assertEqual(result[2]["version"], 2) 182 | 183 | def test_RelationRelations(self): 184 | self._session_mock() 185 | 186 | result = self.api.RelationRelations(1532552) 187 | 188 | args, kwargs = self.session_mock.request.call_args 189 | self.assertEqual(args[0], "GET") 190 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/relation/1532552/relations") 191 | 192 | self.assertEqual(len(result), 1) 193 | self.assertEqual(result[0]["id"], 1532553) 194 | self.assertEqual(result[0]["version"], 85) 195 | self.assertEqual(len(result[0]["member"]), 120) 196 | self.assertEqual(result[0]["tag"]["type"], "network") 197 | self.assertEqual(result[0]["tag"]["name"], "Aargauischer Radroutennetz") 198 | 199 | def test_RelationRelationsUnusedElement(self): 200 | self._session_mock() 201 | 202 | result = self.api.RelationRelations(1532552) 203 | 204 | args, kwargs = self.session_mock.request.call_args 205 | self.assertEqual(args[0], "GET") 206 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/relation/1532552/relations") 207 | 208 | self.assertEqual(len(result), 0) 209 | self.assertIsInstance(result, list) 210 | 211 | def test_RelationFull(self): 212 | self._session_mock() 213 | 214 | result = self.api.RelationFull(2470397) 215 | 216 | args, kwargs = self.session_mock.request.call_args 217 | self.assertEqual(args[0], "GET") 218 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/relation/2470397/full") 219 | 220 | self.assertEqual(len(result), 11) 221 | self.assertEqual(result[1]["data"]["id"], 101142277) 222 | self.assertEqual(result[1]["data"]["version"], 8) 223 | self.assertEqual(result[1]["type"], "node") 224 | self.assertEqual(result[10]["data"]["id"], 2470397) 225 | self.assertEqual(result[10]["data"]["version"], 2) 226 | self.assertEqual(result[10]["type"], "relation") 227 | 228 | def test_RelationsGet(self): 229 | self._session_mock() 230 | 231 | result = self.api.RelationsGet([1532552, 1532553]) 232 | 233 | args, kwargs = self.session_mock.request.call_args 234 | self.assertEqual(args[0], "GET") 235 | self.assertEqual( 236 | args[1], f"{self.api_base}/api/0.6/relations?relations=1532552,1532553" 237 | ) 238 | 239 | self.assertEqual(len(result), 2) 240 | self.assertEqual(result[1532553]["id"], 1532553) 241 | self.assertEqual(result[1532553]["version"], 85) 242 | self.assertEqual(result[1532553]["user"], "SimonPoole") 243 | self.assertEqual(result[1532552]["id"], 1532552) 244 | self.assertEqual(result[1532552]["visible"], True) 245 | self.assertEqual(result[1532552]["tag"]["route"], "bicycle") 246 | 247 | def test_RelationFull_with_deleted_relation(self): 248 | self._session_mock(filenames=[], status=410) 249 | 250 | with self.assertRaises(osmapi.ElementDeletedApiError) as context: 251 | self.api.RelationFull(2911456) 252 | self.assertEqual(410, context.exception.status) 253 | -------------------------------------------------------------------------------- /tests/way_test.py: -------------------------------------------------------------------------------- 1 | from . import osmapi_test 2 | import osmapi 3 | from unittest import mock 4 | import datetime 5 | 6 | 7 | class TestOsmApiWay(osmapi_test.TestOsmApi): 8 | def test_WayGet(self): 9 | self._session_mock() 10 | 11 | result = self.api.WayGet(321) 12 | 13 | args, kwargs = self.session_mock.request.call_args 14 | self.assertEqual(args[0], "GET") 15 | self.assertEqual(args[1], self.api_base + "/api/0.6/way/321") 16 | 17 | self.assertEqual( 18 | result, 19 | { 20 | "id": 321, 21 | "changeset": 298, 22 | "uid": 12, 23 | "timestamp": datetime.datetime(2009, 9, 14, 23, 23, 18), 24 | "visible": True, 25 | "version": 1, 26 | "user": "green525", 27 | "tag": { 28 | "admin_level": "9", 29 | "boundary": "administrative", 30 | }, 31 | "nd": [ 32 | 11949, 33 | 11950, 34 | 11951, 35 | 11952, 36 | 11953, 37 | 11954, 38 | 11955, 39 | 11956, 40 | 11957, 41 | 11958, 42 | 11959, 43 | 11960, 44 | 11961, 45 | 11962, 46 | 11963, 47 | 11964, 48 | 11949, 49 | ], 50 | }, 51 | ) 52 | 53 | def test_WayGet_with_version(self): 54 | self._session_mock() 55 | 56 | result = self.api.WayGet(4294967296, 2) 57 | 58 | args, kwargs = self.session_mock.request.call_args 59 | self.assertEqual(args[0], "GET") 60 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/way/4294967296/2") 61 | 62 | self.assertEqual(result["id"], 4294967296) 63 | self.assertEqual(result["changeset"], 41303) 64 | self.assertEqual(result["user"], "metaodi") 65 | 66 | def test_WayGet_nodata(self): 67 | self._session_mock() 68 | 69 | with self.assertRaises(osmapi.ResponseEmptyApiError): 70 | self.api.WayGet(321) 71 | 72 | def test_WayCreate(self): 73 | self._session_mock(auth=True) 74 | 75 | # setup mock 76 | self.api.ChangesetCreate = mock.Mock(return_value=2222) 77 | self.api._CurrentChangesetId = 2222 78 | 79 | test_way = { 80 | "nd": [11949, 11950], 81 | "tag": {"highway": "unclassified", "name": "Osmapi Street"}, 82 | } 83 | 84 | cs = self.api.ChangesetCreate({"comment": "This is a test way"}) 85 | self.assertEqual(cs, 2222) 86 | 87 | result = self.api.WayCreate(test_way) 88 | 89 | args, kwargs = self.session_mock.request.call_args 90 | self.assertEqual(args[0], "PUT") 91 | self.assertEqual(args[1], self.api_base + "/api/0.6/way/create") 92 | 93 | self.assertEqual(result["id"], 5454) 94 | self.assertEqual(result["nd"], test_way["nd"]) 95 | self.assertEqual(result["tag"], test_way["tag"]) 96 | 97 | def test_WayCreate_existing_node(self): 98 | # setup mock 99 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 100 | self.api._CurrentChangesetId = 1111 101 | 102 | test_way = { 103 | "id": 456, 104 | "nd": [11949, 11950], 105 | "tag": {"highway": "unclassified", "name": "Osmapi Street"}, 106 | } 107 | 108 | with self.assertRaisesRegex( 109 | osmapi.OsmTypeAlreadyExistsError, "This way already exists" 110 | ): 111 | self.api.WayCreate(test_way) 112 | 113 | def test_WayUpdate(self): 114 | self._session_mock(auth=True) 115 | 116 | # setup mock 117 | self.api.ChangesetCreate = mock.Mock(return_value=2222) 118 | self.api._CurrentChangesetId = 2222 119 | 120 | test_way = { 121 | "id": 876, 122 | "nd": [11949, 11950], 123 | "tag": {"highway": "unclassified", "name": "Osmapi Street Update"}, 124 | } 125 | 126 | cs = self.api.ChangesetCreate({"comment": "This is a test way"}) 127 | self.assertEqual(cs, 2222) 128 | 129 | result = self.api.WayUpdate(test_way) 130 | 131 | args, kwargs = self.session_mock.request.call_args 132 | self.assertEqual(args[0], "PUT") 133 | self.assertEqual(args[1], self.api_base + "/api/0.6/way/876") 134 | 135 | self.assertEqual(result["id"], 876) 136 | self.assertEqual(result["version"], 7) 137 | self.assertEqual(result["nd"], test_way["nd"]) 138 | self.assertEqual(result["tag"], test_way["tag"]) 139 | 140 | def test_WayUpdatePreconditionFailed(self): 141 | self._session_mock(auth=True, status=412) 142 | 143 | self.api.ChangesetCreate = mock.Mock(return_value=1111) 144 | self.api._CurrentChangesetId = 1111 145 | 146 | test_way = { 147 | "id": 876, 148 | "nd": [11949, 11950], 149 | "tag": {"highway": "unclassified", "name": "Osmapi Street Update"}, 150 | } 151 | 152 | self.api.ChangesetCreate({"comment": "This is a test dataset"}) 153 | 154 | with self.assertRaises(osmapi.PreconditionFailedApiError) as cm: 155 | self.api.WayUpdate(test_way) 156 | 157 | self.assertEqual(cm.exception.status, 412) 158 | self.assertEqual( 159 | cm.exception.payload, 160 | ( 161 | "Way 876 requires the nodes with id in (11950), " 162 | "which either do not exist, or are not visible." 163 | ), 164 | ) 165 | 166 | def test_WayDelete(self): 167 | self._session_mock(auth=True) 168 | 169 | # setup mock 170 | self.api.ChangesetCreate = mock.Mock(return_value=2222) 171 | self.api._CurrentChangesetId = 2222 172 | 173 | test_way = {"id": 876} 174 | 175 | cs = self.api.ChangesetCreate({"comment": "This is a test way delete"}) 176 | self.assertEqual(cs, 2222) 177 | 178 | result = self.api.WayDelete(test_way) 179 | 180 | args, kwargs = self.session_mock.request.call_args 181 | self.assertEqual(args[0], "DELETE") 182 | self.assertEqual(args[1], self.api_base + "/api/0.6/way/876") 183 | self.assertEqual(result["id"], 876) 184 | self.assertEqual(result["version"], 8) 185 | 186 | def test_WayHistory(self): 187 | self._session_mock() 188 | 189 | result = self.api.WayHistory(4294967296) 190 | 191 | args, kwargs = self.session_mock.request.call_args 192 | self.assertEqual(args[0], "GET") 193 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/way/4294967296/history") 194 | 195 | self.assertEqual(len(result), 2) 196 | self.assertEqual(result[1]["id"], 4294967296) 197 | self.assertEqual(result[1]["version"], 1) 198 | self.assertEqual( 199 | result[1]["tag"], 200 | { 201 | "highway": "unclassified", 202 | "name": "Stansted Road", 203 | }, 204 | ) 205 | 206 | def test_WayRelations(self): 207 | self._session_mock() 208 | 209 | result = self.api.WayRelations(4295032193) 210 | 211 | args, kwargs = self.session_mock.request.call_args 212 | self.assertEqual(args[0], "GET") 213 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/way/4295032193/relations") 214 | 215 | self.assertEqual(len(result), 1) 216 | self.assertEqual(result[0]["id"], 4294968148) 217 | self.assertEqual(result[0]["changeset"], 23123) 218 | self.assertEqual( 219 | result[0]["member"][4], 220 | { 221 | "role": "", 222 | "ref": 4295032193, 223 | "type": "way", 224 | }, 225 | ) 226 | self.assertEqual( 227 | result[0]["tag"], 228 | { 229 | "type": "fancy", 230 | }, 231 | ) 232 | 233 | def test_WayRelationsUnusedElement(self): 234 | self._session_mock() 235 | 236 | result = self.api.WayRelations(4295032193) 237 | 238 | args, kwargs = self.session_mock.request.call_args 239 | self.assertEqual(args[0], "GET") 240 | self.assertEqual(args[1], self.api_base + "/api/0.6/way/4295032193/relations") 241 | 242 | self.assertEqual(len(result), 0) 243 | self.assertIsInstance(result, list) 244 | 245 | def test_WayFull(self): 246 | self._session_mock() 247 | 248 | result = self.api.WayFull(321) 249 | 250 | args, kwargs = self.session_mock.request.call_args 251 | self.assertEqual(args[0], "GET") 252 | self.assertEqual(args[1], self.api_base + "/api/0.6/way/321/full") 253 | 254 | self.assertEqual(len(result), 17) 255 | self.assertEqual(result[0]["data"]["id"], 11949) 256 | self.assertEqual(result[0]["data"]["changeset"], 298) 257 | self.assertEqual(result[0]["type"], "node") 258 | self.assertEqual(result[16]["data"]["id"], 321) 259 | self.assertEqual(result[16]["data"]["changeset"], 298) 260 | self.assertEqual(result[16]["type"], "way") 261 | 262 | def test_WayFull_invalid_response(self): 263 | self._session_mock() 264 | 265 | with self.assertRaises(osmapi.XmlResponseInvalidError): 266 | self.api.WayFull(321) 267 | 268 | def test_WaysGet(self): 269 | self._session_mock() 270 | 271 | result = self.api.WaysGet([456, 678]) 272 | 273 | args, kwargs = self.session_mock.request.call_args 274 | self.assertEqual(args[0], "GET") 275 | self.assertEqual(args[1], f"{self.api_base}/api/0.6/ways?ways=456,678") 276 | 277 | self.assertEqual(len(result), 2) 278 | self.assertIs(type(result[456]), dict) 279 | self.assertIs(type(result[678]), dict) 280 | with self.assertRaises(KeyError): 281 | self.assertIs(type(result[123]), dict) 282 | --------------------------------------------------------------------------------