[Git][debian-gis-team/python-osmapi][upstream] New upstream version 2.0.0

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Tue Nov 23 05:15:35 GMT 2021



Bas Couwenberg pushed to branch upstream at Debian GIS Project / python-osmapi


Commits:
79856c6f by Bas Couwenberg at 2021-11-23T06:04:07+01:00
New upstream version 2.0.0
- - - - -


27 changed files:

- + .github/workflows/build.yml
- + .github/workflows/publish_python.yml
- − .travis.yml
- CHANGELOG.md
- README.md
- build.sh
- osmapi/OsmApi.py
- osmapi/__init__.py
- + osmapi/errors.py
- requirements.txt
- setup.cfg
- setup.py
- + setup.sh
- test-requirements.txt
- tests/changeset_tests.py
- + tests/fixtures/test_NodeRelationsUnusedElement.xml
- + tests/fixtures/test_NodeUpdateConflict.xml
- + tests/fixtures/test_NodeUpdateWhenChangesetIsClosed.xml
- + tests/fixtures/test_NodeWaysNotExists.xml
- + tests/fixtures/test_RelationRelationsUnusedElement.xml
- + tests/fixtures/test_WayRelationsUnusedElement.xml
- + tests/fixtures/test_WayUpdatePreconditionFailed.xml
- tests/node_tests.py
- tests/osmapi_tests.py
- tests/relation_tests.py
- tests/way_tests.py
- tox.ini


Changes:

=====================================
.github/workflows/build.yml
=====================================
@@ -0,0 +1,38 @@
+name: Build osmapi
+on:
+  pull_request:
+  push:
+    branches: [master]
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: [3.7, 3.8, 3.9]
+
+    steps:
+    - uses: actions/checkout at v2
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python at v1
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Install dependencies
+      run: |
+        sudo apt-get install pandoc
+        python -m pip install --upgrade pip
+        pip install -r requirements.txt
+        pip install -r test-requirements.txt
+        pip install -e .
+
+    - name: Build the package 
+      run: ./build.sh
+
+    - name: Test coverage
+      run: coveralls --service=github
+      if: ${{ matrix.python-version == '3.8' }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


=====================================
.github/workflows/publish_python.yml
=====================================
@@ -0,0 +1,29 @@
+# workflow inspired by chezou/tabula-py
+name: Upload Python Package
+
+on:
+  push:
+    # Sequence of patterns matched against refs/tags
+    tags:
+      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout at v1
+    - name: Set up Python
+      uses: actions/setup-python at v1
+      with:
+        python-version: '3.8'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install setuptools wheel twine
+    - name: Build and publish
+      env:
+        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+      run: |
+        python setup.py sdist bdist_wheel
+        twine upload dist/*


=====================================
.travis.yml deleted
=====================================
@@ -1,36 +0,0 @@
-language: python
-
-python:
-- '2.7'
-- '3.4'
-- '3.5'
-- '3.6'
-
-matrix:
-  include:
-  - python: '3.7'
-    dist: xenial    # required for Python 3.7 (travis-ci/travis-ci#9069)
-    sudo: required  # required for Python 3.7 (travis-ci/travis-ci#9069)
-
-before_install:
-- sudo apt-get update -qq
-- sudo apt-get install -qq pandoc
-
-install:
-- pip install -r requirements.txt -r test-requirements.txt
-- pip install .
-
-script: ./build.sh
-
-after_success: coveralls
-
-deploy:
-  - provider: pypi
-    user: odi
-    password:
-      secure: "EfW0Fje7pn/iLv2+eiFGc/PwD2jcdfF5Df9uUKWTCRP8wqMrFriErTczlWqfEIARZDnxQXU5c+UWEQvZ/5D5/4ZctJyvMcmgYi7I1ECXVwYQh0c5prsFl+9uynNJQWfW404v5Kb3MJE1iGaSEOeonObeqThQGdtDTPYZd9H4j20="
-    on:
-      tags: true
-      all_branches: true
-      python: 3.7
-      repo: metaodi/osmapi


=====================================
CHANGELOG.md
=====================================
@@ -2,9 +2,24 @@
 All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project follows [Semantic Versioning](http://semver.org/).
 
-## [Unreleased][unreleased]
+## [Unreleased]
 
-## 1.3.0 - 2020-10-05
+## [2.0.0] - 2021-11-22
+### Added
+- Move from Travis CI to Github Actions
+- Add more API-specific errors to catch specific errors (see issue #115, thanks [Mateusz Konieczny](https://github.com/matkoniecz)):
+    - `ChangesetClosedApiError`
+    - `NoteClosedApiError`
+    - `VersionMismatchApiError`
+    - `PreconditionFailedApiError`
+
+### Changed
+- **BC-Break**: osmapi does **not** support Python 2.7, 3.3, 3.4, 3.5 and 3.6 anymore
+
+### Fixed
+- 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)
+
+## [1.3.0] - 2020-10-05
 ### Added
 - Add close() method to close the underlying http session (see issue #107)
 - Add context manager to automatically open and close the http session (see issue #107)
@@ -12,15 +27,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 ### Fixed
 - Correctly parse password file (thanks [Julien Palard](https://github.com/JulienPalard), see pull request #106)
 
-## 1.2.2 - 2018-11-05
+## [1.2.2] - 2018-11-05
 ### Fixed
 - Update PyPI password for deployment
 
-## 1.2.1 - 2018-11-05
+## [1.2.1] - 2018-11-05
 ### Fixed
 - Deployment to PyPI with Travis
 
-## 1.2.0 - 2018-11-05
+## [1.2.0] - 2018-11-05
 ### Added
 - Support Python 3.7 (thanks a lot [cclauss](https://github.com/cclauss))
 
@@ -31,25 +46,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 - Updated dependencies for Python 3.7
 - Adapt README to use Python 3 syntax (thanks [cclauss](https://github.com/cclauss))
 
-## 1.1.0 - 2017-10-11
+## [1.1.0] - 2017-10-11
 ### Added
 - Raise new `XmlResponseInvalidError` if XML response from the OpenStreetMap API is invalid
 
 ### Changed
 - Improved README (thanks [Mateusz Konieczny](https://github.com/matkoniecz))
 
-## 1.0.2 - 2017-09-07
+## [1.0.2] - 2017-09-07
 ### Added
 - Rais ResponseEmptyApiError if we expect a response from the OpenStreetMap API, but didn't get one
 
 ### Removed
 - Removed httpretty as HTTP mock library
 
-## 1.0.1 - 2017-09-07
+## [1.0.1] - 2017-09-07
 ### Fixed
 - Make sure tests run offline
 
-## 1.0.0 - 2017-09-05
+## [1.0.0] - 2017-09-05
 ### Added
 - Officially support Python 3.5 and 3.6
 
@@ -59,41 +74,41 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 ### Changed
 - **BC-Break:** raise an exception if the requested element is deleted (previoulsy `None` has been returned)
 
-## 0.8.1 - 2016-12-21
+## [0.8.1] - 2016-12-21
 ### Fixed
 - Use setuptools instead of distutils in setup.py
 
-## 0.8.0 - 2016-12-21
+## [0.8.0] - 2016-12-21
 ### Removed
 - This release no longer supports Python 3.2, if you need it, go back to release <= 0.6.2
 
 ## Changed
 - Read version from __init__.py instead of importing it in setup.py
 
-## 0.7.2 - 2016-12-21
+## [0.7.2] - 2016-12-21
 ### Fixed
 - Added 'requests' as a dependency to setup.py to fix installation problems
 
-## 0.7.1 - 2016-12-12
+## [0.7.1] - 2016-12-12
 ### Changed
 - Catch OSError in setup.py to avoid installation errors
 
-## 0.7.0 - 2016-12-07
+## [0.7.0] - 2016-12-07
 ### Changed
 - Replace the old httplib with requests library (thanks a lot [Austin Hartzheim](http://austinhartzheim.me/)!)
 - Use format strings instead of ugly string concatenation
 - Fix unicode in changesets (thanks a lot to [MichaelVL](https://github.com/MichaelVL)!)
 
-## 0.6.2 - 2016-01-04
+## [0.6.2] - 2016-01-04
 ### Changed
 - Re-arranged README
 - Make sure PyPI releases are only created when a release has been tagged on GitHub
 
-## 0.6.1 - 2016-01-04
+## [0.6.1] - 2016-01-04
 ### Changed
 - The documentation is now available at a new domain: http://osmapi.metaodi.ch, the previous provider does no longer provide this service
 
-## 0.6.0 - 2015-05-26
+## [0.6.0] - 2015-05-26
 ### Added
 - SSL support for the API calls (thanks [Austin Hartzheim](http://austinhartzheim.me/)!)
 - Run tests on Python 3.4 as well
@@ -104,7 +119,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 ### Changed
 - Changed generic `Exception` with more specific ones, so a client can catch those and react accordingly (no BC-break!)
 
-## 0.5.0 - 2015-01-03
+## [0.5.0] - 2015-01-03
 ### Changed
 - BC-break: all dates are now parsed as datetime objects
 
@@ -113,7 +128,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 - When (un)subscribing to a changeset, there are two special errors `AlreadySubscribedApiError` and `NotSubscribedApiError` to check for
 - The ChangesetGet method got a new parameter `include_discussion` to determine wheter or not changeset discussion should be in the response
 
-## 0.4.2 - 2015-01-01
+## [0.4.2] - 2015-01-01
 ### Fixed
 - Result of `NodeWay` is now actually parsed as a `way`
 
@@ -123,54 +138,54 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 ### Changed
 - Update to pdoc 0.3.1 which changed the appearance of the online docs
 
-## 0.4.1 - 2014-10-08
+## [0.4.1] - 2014-10-08
 ### Changed
 - Parse dates in notes as `datetime` objects
 
-## 0.4.0 - 2014-10-07
+## [0.4.0] - 2014-10-07
 ### Added
 - Release for OSM Notes API
 - Generation of online documentation (http://osmapi.divshot.io)
 
-## 0.3.1 - 2014-06-21
+## [0.3.1] - 2014-06-21
 ### Fixed
 - Hotfix release of Python 3.x (base64)
 
-## 0.3.0 - 2014-05-20
+## [0.3.0] - 2014-05-20
 ### Added
 - Support for Python 3.x
 - Use `tox` to run tests against multiple versions of Python
 
-## 0.2.26 - 2014-05-02
+## [0.2.26] - 2014-05-02
 ### Fixed
 - Fixed notes again
 
-## 0.2.25 - 2014-05-02
+## [0.2.25] - 2014-05-02
 ### Fixed
 - Unit tests for basic functionality
 - Fixed based on the unit tests (previously undetected bugs)
 
-## 0.2.24 - 2014-01-07
+## [0.2.24] - 2014-01-07
 ### Fixed
 - Fixed notes
 
-## 0.2.23 - 2014-01-03
+## [0.2.23] - 2014-01-03
 ### Changed
 - Hotfix release
 
-## 0.2.22 - 2014-01-03
+## [0.2.22] - 2014-01-03
 ### Fixed
 - Fixed README.md not found error during installation
 
-## 0.2.21 - 2014-01-03
+## [0.2.21] - 2014-01-03
 ### Changed
 - Updated description
 
-## 0.2.20 - 2014-01-01
+## [0.2.20] - 2014-01-01
 ### Added
 - First release of PyPI package "osmapi"
 
-## 0.2.19 - 2014-01-01
+## [0.2.19] - 2014-01-01
 ### Changed
 - Inital version from SVN (http://svn.openstreetmap.org/applications/utils/python_lib/OsmApi/OsmApi.py)
 - Move to GitHub
@@ -265,3 +280,37 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 - `Removed` for deprecated features removed in this release.
 - `Fixed` for any bug fixes.
 - `Security` to invite users to upgrade in case of vulnerabilities.
+
+[Unreleased]: https://github.com/metaodi/osmapi/compare/v2.0.0...HEAD
+[2.0.0]: https://github.com/metaodi/osmapi/compare/v1.3.0...v2.0.0
+[1.3.0]: https://github.com/metaodi/osmapi/compare/v1.2.2...v1.3.0
+[1.2.2]: https://github.com/metaodi/osmapi/compare/v1.2.1...v1.2.2
+[1.2.1]: https://github.com/metaodi/osmapi/compare/v1.2.0...v1.2.1
+[1.2.0]: https://github.com/metaodi/osmapi/compare/v1.1.0...v1.2.0
+[1.1.0]: https://github.com/metaodi/osmapi/compare/v1.0.2...v1.1.0
+[1.0.2]: https://github.com/metaodi/osmapi/compare/v1.0.1...v1.0.2
+[1.0.1]: https://github.com/metaodi/osmapi/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.com/metaodi/osmapi/compare/v0.8.1...v1.0.0
+[0.8.1]: https://github.com/metaodi/osmapi/compare/v0.8.0...v0.8.1
+[0.8.0]: https://github.com/metaodi/osmapi/compare/v0.7.2...v0.8.0
+[0.7.2]: https://github.com/metaodi/osmapi/compare/v0.7.1...v0.7.2
+[0.7.1]: https://github.com/metaodi/osmapi/compare/v0.7.0...v0.7.1
+[0.7.0]: https://github.com/metaodi/osmapi/compare/v0.6.2...v0.7.0
+[0.6.2]: https://github.com/metaodi/osmapi/compare/v0.6.1...v0.6.2
+[0.6.1]: https://github.com/metaodi/osmapi/compare/v0.6.0...v0.6.1
+[0.6.0]: https://github.com/metaodi/osmapi/compare/v0.5.0...v0.6.0
+[0.5.0]: https://github.com/metaodi/osmapi/compare/v0.4.2...v0.5.0
+[0.4.2]: https://github.com/metaodi/osmapi/compare/v0.4.1...v0.4.2
+[0.4.1]: https://github.com/metaodi/osmapi/compare/v0.4.0...v0.4.1
+[0.4.0]: https://github.com/metaodi/osmapi/compare/v0.3.1...v0.4.0
+[0.3.1]: https://github.com/metaodi/osmapi/compare/v0.3.0...v0.3.1
+[0.3.0]: https://github.com/metaodi/osmapi/compare/v0.2.26...v0.3.0
+[0.2.26]: https://github.com/metaodi/osmapi/compare/v0.2.25...v0.2.26
+[0.2.25]: https://github.com/metaodi/osmapi/compare/v0.2.24...v0.2.25
+[0.2.24]: https://github.com/metaodi/osmapi/compare/v0.2.23...v0.2.24
+[0.2.23]: https://github.com/metaodi/osmapi/compare/v0.2.22...v0.2.23
+[0.2.22]: https://github.com/metaodi/osmapi/compare/v0.2.21...v0.2.22
+[0.2.21]: https://github.com/metaodi/osmapi/compare/v0.2.20...v0.2.21
+[0.2.20]: https://github.com/metaodi/osmapi/compare/v0.2.19...v0.2.20
+[0.2.19]: https://github.com/metaodi/osmapi/releases/tag/v0.2.19
+


=====================================
README.md
=====================================
@@ -1,12 +1,12 @@
 osmapi
 ======
 
-[![Build](https://img.shields.io/travis/metaodi/osmapi/develop.svg)](https://travis-ci.org/metaodi/osmapi)
+[![Build osmapi](https://github.com/metaodi/osmapi/actions/workflows/build.yml/badge.svg)](https://github.com/metaodi/osmapi/actions/workflows/build.yml)
 [![Coverage](https://img.shields.io/coveralls/metaodi/osmapi/develop.svg)](https://coveralls.io/r/metaodi/osmapi?branch=develop)
 [![Version](https://img.shields.io/pypi/v/osmapi.svg)](https://pypi.python.org/pypi/osmapi/)
 [![License](https://img.shields.io/pypi/l/osmapi.svg)](https://github.com/metaodi/osmapi/blob/master/LICENSE.txt)
 
-Python wrapper for the OSM API
+Python wrapper for the OSM API (requires Python >= 3.7)
 
 ## Installation
 
@@ -20,7 +20,7 @@ The documentation is generated using `pdoc` and can be [viewed online](http://os
 
 The build the documentation locally, you can use
 
-    pdoc --html osmapi.OsmApi # create HTML file
+    pdoc -o . osmapi # create HTML files
 
 This project uses GitHub Pages to publish its documentation.
 To update the online documentation, you need to re-generate the documentation with the above command and update the `gh-pages` branch of this repository.
@@ -87,7 +87,7 @@ To run the tests use the following command:
 
     nosetests --verbose
 
-By using tox you can even run the tests against different versions of python (2.7, 3.4, 3.5, 3.6 and 3.7):
+By using tox you can even run the tests against different versions of python (3.7, 3.8, 3.9):
 
     tox
 
@@ -99,8 +99,8 @@ To create a new release, follow these steps (please respect [Semantic Versioning
 1. Update the CHANGELOG with the version
 1. Create a pull request to merge develop into master (make sure the tests pass!)
 1. Create a [new release/tag on GitHub](https://github.com/metaodi/osmapi/releases) (on the master branch)
-1. The [publication on PyPI](https://pypi.python.org/pypi/osmapi) happens via [Travis CI](https://travis-ci.org/metaodi/osmapi) on every tagged commit
-1. Re-build the documentation (see above) and copy the generated file to `index.html` on the `gh-pages` branch
+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
+1. Re-build the documentation (see above) and copy the generated files to the `gh-pages` branch
 
 ## Attribution
 


=====================================
build.sh
=====================================
@@ -15,7 +15,7 @@ flake8 --statistics --show-source .
 nosetests --verbose --with-coverage
 
 # generate the docs
-pdoc --html --overwrite osmapi/OsmApi.py
+pdoc -o . osmapi
 
 # setup a new virtualenv and try to install the lib
 virtualenv pyenv


=====================================
osmapi/OsmApi.py
=====================================
@@ -27,120 +27,32 @@ Find all information about changes of the different versions of this module
 
 """
 
-from __future__ import (absolute_import, print_function, unicode_literals)
 import xml.dom.minidom
 import xml.parsers.expat
 import time
 import sys
-import urllib
+import urllib.parse
+import re
 import requests
 from datetime import datetime
 
 from osmapi import __version__
-
-# Python 3.x
-if getattr(urllib, 'urlencode', None) is None:
-    urllib.urlencode = urllib.parse.urlencode
-
-
-class OsmApiError(Exception):
-    """
-    General OsmApi error class to provide a superclass for all other errors
-    """
-
-
-class MaximumRetryLimitReachedError(OsmApiError):
-    """
-    Error when the maximum amount of retries is reached and we have to give up
-    """
-
-
-class UsernamePasswordMissingError(OsmApiError):
-    """
-    Error when username or password is missing for an authenticated request
-    """
-    pass
-
-
-class NoChangesetOpenError(OsmApiError):
-    """
-    Error when an operation requires an open changeset, but currently
-    no changeset _is_ open
-    """
-    pass
-
-
-class ChangesetAlreadyOpenError(OsmApiError):
-    """
-    Error when a user tries to open a changeset when there is already
-    an open changeset
-    """
-    pass
-
-
-class OsmTypeAlreadyExistsError(OsmApiError):
-    """
-    Error when a user tries to create an object that already exsits
-    """
-    pass
-
-
-class XmlResponseInvalidError(OsmApiError):
-    """
-    Error if the XML response from the OpenStreetMap API is invalid
-    """
-
-
-class ApiError(OsmApiError):
-    """
-    Error class, is thrown when an API request fails
-    """
-
-    def __init__(self, status, reason, payload):
-        self.status = status
-        """HTTP error code"""
-
-        self.reason = reason
-        """Error message"""
-
-        self.payload = payload
-        """Payload of API when this error occured"""
-
-    def __str__(self):
-        return (
-            "Request failed: %s - %s - %s"
-            % (str(self.status), self.reason, self.payload)
-        )
-
-
-class AlreadySubscribedApiError(ApiError):
-    """
-    Error when a user tries to subscribe to a changeset
-    that she is already subscribed to
-    """
-    pass
-
-
-class NotSubscribedApiError(ApiError):
-    """
-    Error when user tries to unsubscribe from a changeset
-    that he is not subscribed to
-    """
-    pass
-
-
-class ElementDeletedApiError(ApiError):
-    """
-    Error when the requested element is deleted
-    """
-    pass
-
-
-class ResponseEmptyApiError(ApiError):
-    """
-    Error when the response to the request is empty
-    """
-    pass
+from .errors import AlreadySubscribedApiError
+from .errors import ApiError
+from .errors import ChangesetAlreadyOpenError
+from .errors import ChangesetClosedApiError
+from .errors import ElementDeletedApiError
+from .errors import MaximumRetryLimitReachedError
+from .errors import NoChangesetOpenError
+from .errors import NotSubscribedApiError
+from .errors import NoteClosedApiError
+from .errors import OsmApiError
+from .errors import OsmTypeAlreadyExistsError
+from .errors import PreconditionFailedApiError
+from .errors import ResponseEmptyApiError
+from .errors import UsernamePasswordMissingError
+from .errors import VersionMismatchApiError
+from .errors import XmlResponseInvalidError
 
 
 class OsmApi:
@@ -396,6 +308,9 @@ class OsmApi:
 
         If the supplied information contain an existing node,
         `OsmApi.OsmTypeAlreadyExistsError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         return self._do("create", "node", NodeData)
 
@@ -435,6 +350,9 @@ class OsmApi:
 
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         return self._do("modify", "node", NodeData)
 
@@ -475,6 +393,9 @@ class OsmApi:
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
 
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
+
         If the requested element has already been deleted,
         `OsmApi.ElementDeletedApiError` is raised.
         """
@@ -527,7 +448,7 @@ class OsmApi:
         """
         uri = "/api/0.6/node/%d/ways" % NodeId
         data = self._get(uri)
-        ways = self._OsmResponseToDom(data, tag="way")
+        ways = self._OsmResponseToDom(data, tag="way", allow_empty=True)
         result = []
         for way in ways:
             data = self._DomParseWay(way)
@@ -568,7 +489,7 @@ class OsmApi:
         """
         uri = "/api/0.6/node/%d/relations" % NodeId
         data = self._get(uri)
-        relations = self._OsmResponseToDom(data, tag="relation")
+        relations = self._OsmResponseToDom(data, tag="relation", allow_empty=True)
         result = []
         for relation in relations:
             data = self._DomParseRelation(relation)
@@ -669,6 +590,9 @@ class OsmApi:
 
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         return self._do("create", "way", WayData)
 
@@ -706,6 +630,9 @@ class OsmApi:
 
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         return self._do("modify", "way", WayData)
 
@@ -744,6 +671,9 @@ class OsmApi:
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
 
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
+
         If the requested element has already been deleted,
         `OsmApi.ElementDeletedApiError` is raised.
         """
@@ -805,7 +735,7 @@ class OsmApi:
         """
         uri = "/api/0.6/way/%d/relations" % WayId
         data = self._get(uri)
-        relations = self._OsmResponseToDom(data, tag="relation")
+        relations = self._OsmResponseToDom(data, tag="relation", allow_empty=True)
         result = []
         for relation in relations:
             data = self._DomParseRelation(relation)
@@ -945,6 +875,9 @@ class OsmApi:
 
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         return self._do("create", "relation", RelationData)
 
@@ -991,6 +924,9 @@ class OsmApi:
 
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         return self._do("modify", "relation", RelationData)
 
@@ -1038,6 +974,9 @@ class OsmApi:
         If there is already an open changeset,
         `OsmApi.ChangesetAlreadyOpenError` is raised.
 
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
+
         If the requested element has already been deleted,
         `OsmApi.ElementDeletedApiError` is raised.
         """
@@ -1100,7 +1039,7 @@ class OsmApi:
         """
         uri = "/api/0.6/relation/%d/relations" % RelationId
         data = self._get(uri)
-        relations = self._OsmResponseToDom(data, tag="relation")
+        relations = self._OsmResponseToDom(data, tag="relation", allow_empty=True)
         result = []
         for relation in relations:
             data = self._DomParseRelation(relation)
@@ -1243,16 +1182,25 @@ class OsmApi:
 
         If there is no open changeset,
         `OsmApi.NoChangesetOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         if not self._CurrentChangesetId:
             raise NoChangesetOpenError("No changeset currently opened")
         if "created_by" not in ChangesetTags:
             ChangesetTags["created_by"] = self._created_by
-        self._put(
-            "/api/0.6/changeset/%s" % (self._CurrentChangesetId),
-            self._XmlBuild("changeset", {"tag": ChangesetTags}),
-            return_value=False
-        )
+        try:
+            self._put(
+                "/api/0.6/changeset/%s" % (self._CurrentChangesetId),
+                self._XmlBuild("changeset", {"tag": ChangesetTags}),
+                return_value=False
+            )
+        except ApiError as e:
+            if e.status == 409:
+                raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
         return self._CurrentChangesetId
 
     def ChangesetCreate(self, ChangesetTags={}):
@@ -1291,16 +1239,25 @@ class OsmApi:
 
         If there is no open changeset,
         `OsmApi.NoChangesetOpenError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         if not self._CurrentChangesetId:
             raise NoChangesetOpenError("No changeset currently opened")
-        self._put(
-            "/api/0.6/changeset/%s/close" % (self._CurrentChangesetId),
-            "",
-            return_value=False
-        )
-        CurrentChangesetId = self._CurrentChangesetId
-        self._CurrentChangesetId = 0
+        try:
+            self._put(
+                "/api/0.6/changeset/%s/close" % (self._CurrentChangesetId),
+                "",
+                return_value=False
+            )
+            CurrentChangesetId = self._CurrentChangesetId
+            self._CurrentChangesetId = 0
+        except ApiError as e:
+            if e.status == 409:
+                raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
         return CurrentChangesetId
 
     def ChangesetUpload(self, ChangesData):
@@ -1318,6 +1275,9 @@ class OsmApi:
 
         If no authentication information are provided,
         `OsmApi.UsernamePasswordMissingError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
         data = ""
         data += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
@@ -1333,10 +1293,16 @@ class OsmApi:
             ).decode("utf-8")
             data += "</" + change["action"] + ">\n"
         data += "</osmChange>"
-        data = self._post(
-            "/api/0.6/changeset/%s/upload" % (self._CurrentChangesetId),
-            data.encode("utf-8")
-        )
+        try:
+            data = self._post(
+                "/api/0.6/changeset/%s/upload" % (self._CurrentChangesetId),
+                data.encode("utf-8")
+            )
+        except ApiError as e:
+            if e.status == 409 and re.search(r"The changeset .* was closed at .*", e.payload):
+                raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
         try:
             data = xml.dom.minidom.parseString(data)
             data = data.getElementsByTagName("diffResult")[0]
@@ -1426,7 +1392,7 @@ class OsmApi:
             params["closed"] = 1
 
         if params:
-            uri += "?" + urllib.urlencode(params)
+            uri += "?" + urllib.parse.urlencode(params)
 
         data = self._get(uri)
         changesets = self._OsmResponseToDom(data, tag="changeset")
@@ -1463,12 +1429,21 @@ class OsmApi:
 
         If no authentication information are provided,
         `OsmApi.UsernamePasswordMissingError` is raised.
+
+        If the changeset is already closed,
+        `OsmApi.ChangesetClosedApiError` is raised.
         """
-        params = urllib.urlencode({'text': comment})
-        data = self._post(
-            "/api/0.6/changeset/%s/comment" % (ChangesetId),
-            params
-        )
+        params = urllib.parse.urlencode({'text': comment})
+        try:
+            data = self._post(
+                "/api/0.6/changeset/%s/comment" % (ChangesetId),
+                params
+            )
+        except ApiError as e:
+            if e.status == 409:
+                raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
         changeset = self._OsmResponseToDom(data, tag="changeset", single=True)
         return self._DomParseChangeset(changeset)
 
@@ -1628,7 +1603,7 @@ class OsmApi:
         Returns updated NoteData (without timestamp).
         """
         uri = "/api/0.6/notes"
-        uri += "?" + urllib.urlencode(NoteData)
+        uri += "?" + urllib.parse.urlencode(NoteData)
         return self._NoteAction(uri)
 
     def NoteComment(self, NoteId, comment):
@@ -1683,7 +1658,7 @@ class OsmApi:
         params['q'] = query
         params['limit'] = limit
         params['closed'] = closed
-        uri += "?" + urllib.urlencode(params)
+        uri += "?" + urllib.parse.urlencode(params)
         data = self._get(uri)
 
         return self.ParseNotes(data)
@@ -1698,8 +1673,14 @@ class OsmApi:
         if comment is not None:
             params = {}
             params['text'] = comment
-            uri += "?" + urllib.urlencode(params)
-        result = self._post(uri, None, optionalAuth=optionalAuth)
+            uri += "?" + urllib.parse.urlencode(params)
+        try:
+            result = self._post(uri, None, optionalAuth=optionalAuth)
+        except ApiError as e:
+            if e.status == 404:
+                raise NoteClosedApiError(e.status, e.reason, e.payload)
+            else:
+                raise
 
         # parse the result
         noteElement = self._OsmResponseToDom(result, tag="note", single=True)
@@ -1861,7 +1842,7 @@ class OsmApi:
         else:
             return self._do_manu(action, OsmType, OsmData)
 
-    def _do_manu(self, action, OsmType, OsmData):
+    def _do_manu(self, action, OsmType, OsmData):  # noqa
         if not self._CurrentChangesetId:
             raise NoChangesetOpenError(
                 "You need to open a changeset before uploading data"
@@ -1874,25 +1855,56 @@ class OsmApi:
                 raise OsmTypeAlreadyExistsError(
                     "This %s already exists" % OsmType
                 )
-            result = self._put(
-                "/api/0.6/%s/create" % OsmType,
-                self._XmlBuild(OsmType, OsmData)
-            )
+            try:
+                result = self._put(
+                    "/api/0.6/%s/create" % OsmType,
+                    self._XmlBuild(OsmType, OsmData)
+                )
+            except ApiError as e:
+                if e.status == 409 and re.search(r"The changeset .* was closed at .*", e.payload):
+                    raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+                elif e.status == 409:
+                    raise VersionMismatchApiError(e.status, e.reason, e.payload)
+                elif e.status == 412:
+                    raise PreconditionFailedApiError(e.status, e.reason, e.payload)
+                else:
+                    raise
             OsmData["id"] = int(result.strip())
             OsmData["version"] = 1
             return OsmData
         elif action == "modify":
-            result = self._put(
-                "/api/0.6/%s/%s" % (OsmType, OsmData["id"]),
-                self._XmlBuild(OsmType, OsmData)
-            )
+            try:
+                result = self._put(
+                    "/api/0.6/%s/%s" % (OsmType, OsmData["id"]),
+                    self._XmlBuild(OsmType, OsmData)
+                )
+            except ApiError as e:
+                print(e.reason)
+                if e.status == 409 and re.search(r"The changeset .* was closed at .*", e.payload):
+                    raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+                elif e.status == 409:
+                    raise VersionMismatchApiError(e.status, e.reason, e.payload)
+                elif e.status == 412:
+                    raise PreconditionFailedApiError(e.status, e.reason, e.payload)
+                else:
+                    raise
             OsmData["version"] = int(result.strip())
             return OsmData
         elif action == "delete":
-            result = self._delete(
-                "/api/0.6/%s/%s" % (OsmType, OsmData["id"]),
-                self._XmlBuild(OsmType, OsmData)
-            )
+            try:
+                result = self._delete(
+                    "/api/0.6/%s/%s" % (OsmType, OsmData["id"]),
+                    self._XmlBuild(OsmType, OsmData)
+                )
+            except ApiError as e:
+                if e.status == 409 and re.search(r"The changeset .* was closed at .*", e.payload):
+                    raise ChangesetClosedApiError(e.status, e.reason, e.payload)
+                elif e.status == 409:
+                    raise VersionMismatchApiError(e.status, e.reason, e.payload)
+                elif e.status == 412:
+                    raise PreconditionFailedApiError(e.status, e.reason, e.payload)
+                else:
+                    raise
             OsmData["version"] = int(result.strip())
             OsmData["visible"] = False
             return OsmData
@@ -2065,7 +2077,7 @@ class OsmApi:
     # Internal dom function                          #
     ##################################################
 
-    def _OsmResponseToDom(self, response, tag, single=False):
+    def _OsmResponseToDom(self, response, tag, single=False, allow_empty=False):
         """
         Returns the (sub-) DOM parsed from an OSM response
         """
@@ -2074,7 +2086,13 @@ class OsmApi:
             osm_dom = dom.getElementsByTagName("osm")[0]
             all_data = osm_dom.getElementsByTagName(tag)
             first_element = all_data[0]
-        except (xml.parsers.expat.ExpatError, IndexError) as e:
+        except (IndexError) as e:
+            if allow_empty:
+                return []
+            raise XmlResponseInvalidError(
+                "The XML response from the OSM API is invalid: %r" % e
+            )
+        except (xml.parsers.expat.ExpatError) as e:
             raise XmlResponseInvalidError(
                 "The XML response from the OSM API is invalid: %r" % e
             )


=====================================
osmapi/__init__.py
=====================================
@@ -1,5 +1,4 @@
-from __future__ import (absolute_import, print_function, unicode_literals)
-
-__version__ = '1.3.0'
+__version__ = '2.0.0'
 
 from .OsmApi import *  # noqa
+from .errors import *  # noqa


=====================================
osmapi/errors.py
=====================================
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+
+class OsmApiError(Exception):
+    """
+    General OsmApi error class to provide a superclass for all other errors
+    """
+
+
+class MaximumRetryLimitReachedError(OsmApiError):
+    """
+    Error when the maximum amount of retries is reached and we have to give up
+    """
+
+
+class UsernamePasswordMissingError(OsmApiError):
+    """
+    Error when username or password is missing for an authenticated request
+    """
+    pass
+
+
+class NoChangesetOpenError(OsmApiError):
+    """
+    Error when an operation requires an open changeset, but currently
+    no changeset _is_ open
+    """
+    pass
+
+
+class ChangesetAlreadyOpenError(OsmApiError):
+    """
+    Error when a user tries to open a changeset when there is already
+    an open changeset
+    """
+    pass
+
+
+class OsmTypeAlreadyExistsError(OsmApiError):
+    """
+    Error when a user tries to create an object that already exsits
+    """
+    pass
+
+
+class XmlResponseInvalidError(OsmApiError):
+    """
+    Error if the XML response from the OpenStreetMap API is invalid
+    """
+
+
+class ApiError(OsmApiError):
+    """
+    Error class, is thrown when an API request fails
+    """
+
+    def __init__(self, status, reason, payload):
+        self.status = status
+        """HTTP error code"""
+
+        self.reason = reason
+        """Error message"""
+
+        self.payload = payload
+        """Payload of API when this error occured"""
+
+    def __str__(self):
+        return (
+            "Request failed: %s - %s - %s"
+            % (str(self.status), self.reason, self.payload)
+        )
+
+
+class AlreadySubscribedApiError(ApiError):
+    """
+    Error when a user tries to subscribe to a changeset
+    that she is already subscribed to
+    """
+    pass
+
+
+class NotSubscribedApiError(ApiError):
+    """
+    Error when user tries to unsubscribe from a changeset
+    that he is not subscribed to
+    """
+    pass
+
+
+class ElementDeletedApiError(ApiError):
+    """
+    Error when the requested element is deleted
+    """
+    pass
+
+
+class ResponseEmptyApiError(ApiError):
+    """
+    Error when the response to the request is empty
+    """
+    pass
+
+
+class ChangesetClosedApiError(ApiError):
+    """
+    Error if the the changeset in question has already been closed
+    """
+
+
+class NoteClosedApiError(ApiError):
+    """
+    Error if the the note in question has already been closed
+    """
+
+
+class VersionMismatchApiError(ApiError):
+    """
+    Error if the provided version does not match the database version
+    of the element
+    """
+
+
+class PreconditionFailedApiError(ApiError):
+    """
+    Error if the precondition of the operation was not met:
+    - When a way has nodes that do not exist or are not visible
+    - When a relation has elements that do not exist or are not visible
+    - When a node/way/relation is still used in a way/relation
+    """


=====================================
requirements.txt
=====================================
@@ -1,7 +1,7 @@
 # This file lists the dependencies of this extension.
 # Install with a command like: pip install -r pip-requirements.txt
-pdoc==0.3.2
-Pygments==2.2.0
-pypandoc==1.4
-requests==2.20.0
-Unidecode==1.0.22
+pdoc==8.0.1
+Pygments==2.10.0
+pypandoc==1.6.4
+requests==2.26.0
+Unidecode==1.3.2


=====================================
setup.cfg
=====================================
@@ -3,3 +3,8 @@ description-file = README.md
 
 [flake8]
 max-complexity = 10
+exclude = .git,.tox,__pycache__,pyenv,build,dist
+# the new Torvalds default for line length
+max-line-length = 100
+# set to true to check all ignored errors
+disable_noqa = False


=====================================
setup.py
=====================================
@@ -41,13 +41,9 @@ setup(
         'Topic :: Scientific/Engineering :: GIS',
         'Topic :: Software Development :: Libraries',
         'Development Status :: 4 - Beta',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
     ],
 )


=====================================
setup.sh
=====================================
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+[ ! -d pyenv ] && python -m venv pyenv
+source pyenv/bin/activate
+
+pip install --upgrade pip
+pip install -r requirements.txt
+pip install -r test-requirements.txt
+pip install -e .


=====================================
test-requirements.txt
=====================================
@@ -1,10 +1,8 @@
-# This file lists the dependencies of this extension.
-# Install with a command like: pip install -r pip-requirements.txt
-coverage==4.5.1
-coveralls==1.5.1
-flake8==3.6.0
-mock==2.0.0
-nose==1.3.7
-tox==3.5.3
-virtualenv==16.1.0
-xmltodict==0.11.0
+coverage
+coveralls
+flake8
+mock
+nose
+tox
+virtualenv
+xmltodict


=====================================
tests/changeset_tests.py
=====================================
@@ -92,10 +92,10 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osm version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osm version="0.6" generator="osmapi/2.0.0">\n'
                 b'  <changeset visible="true">\n'
                 b'    <tag k="test" v="foobar"/>\n'
-                b'    <tag k="created_by" v="osmapi/1.3.0"/>\n'
+                b'    <tag k="created_by" v="osmapi/2.0.0"/>\n'
                 b'  </changeset>\n'
                 b'</osm>\n'
             )
@@ -125,7 +125,7 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osm version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osm version="0.6" generator="osmapi/2.0.0">\n'
                 b'  <changeset visible="true">\n'
                 b'    <tag k="test" v="foobar"/>\n'
                 b'    <tag k="created_by" v="MyTestOSMApp"/>\n'
@@ -163,10 +163,10 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osm version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osm version="0.6" generator="osmapi/2.0.0">\n'
                 b'  <changeset visible="true">\n'
                 b'    <tag k="foobar" v="A new test changeset"/>\n'
-                b'    <tag k="created_by" v="osmapi/1.3.0"/>\n'
+                b'    <tag k="created_by" v="osmapi/2.0.0"/>\n'
                 b'  </changeset>\n'
                 b'</osm>\n'
             )
@@ -190,7 +190,7 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osm version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osm version="0.6" generator="osmapi/2.0.0">\n'
                 b'  <changeset visible="true">\n'
                 b'    <tag k="foobar" v="A new test changeset"/>\n'
                 b'    <tag k="created_by" v="CoolTestApp"/>\n'
@@ -276,7 +276,7 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osmChange version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osmChange version="0.6" generator="osmapi/2.0.0">\n'
                 b'<create>\n'
                 b'  <node lat="47.123" lon="8.555" visible="true" '
                 b'changeset="4444">\n'
@@ -350,7 +350,7 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osmChange version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osmChange version="0.6" generator="osmapi/2.0.0">\n'
                 b'<modify>\n'
                 b'  <way id="4294967296" version="2" visible="true" '
                 b'changeset="4444">\n'
@@ -434,7 +434,7 @@ class TestOsmApiChangeset(osmapi_tests.TestOsmApi):
             xmltosorteddict(kwargs['data']),
             xmltosorteddict(
                 b'<?xml version="1.0" encoding="UTF-8"?>\n'
-                b'<osmChange version="0.6" generator="osmapi/1.3.0">\n'
+                b'<osmChange version="0.6" generator="osmapi/2.0.0">\n'
                 b'<delete>\n'
                 b'  <relation id="676" version="2" visible="true" '
                 b'changeset="4444">\n'


=====================================
tests/fixtures/test_NodeRelationsUnusedElement.xml
=====================================
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+</osm>


=====================================
tests/fixtures/test_NodeUpdateConflict.xml
=====================================
@@ -0,0 +1 @@
+Version does not match the current database version of the element


=====================================
tests/fixtures/test_NodeUpdateWhenChangesetIsClosed.xml
=====================================
@@ -0,0 +1 @@
+The changeset 2222 was closed at 2021-11-20 09:42:47 UTC.


=====================================
tests/fixtures/test_NodeWaysNotExists.xml
=====================================
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+</osm>


=====================================
tests/fixtures/test_RelationRelationsUnusedElement.xml
=====================================
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+</osm>


=====================================
tests/fixtures/test_WayRelationsUnusedElement.xml
=====================================
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<osm version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
+</osm>


=====================================
tests/fixtures/test_WayUpdatePreconditionFailed.xml
=====================================
@@ -0,0 +1 @@
+Way 876 requires the nodes with id in (11950), which either do not exist, or are not visible.


=====================================
tests/node_tests.py
=====================================
@@ -234,6 +234,68 @@ class TestOsmApiNode(osmapi_tests.TestOsmApi):
         self.assertEquals(result['lon'], test_node['lon'])
         self.assertEquals(result['tag'], test_node['tag'])
 
+    def test_NodeUpdateWhenChangesetIsClosed(self):
+        self._session_mock(auth=True, status=409)
+
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+
+        test_node = {
+            'id': 7676,
+            'lat': 47.287,
+            'lon': 8.765,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'name': 'christian'
+            }
+        }
+
+        self.api.ChangesetCreate({
+            'comment': 'This is a test dataset'
+        })
+
+        with self.assertRaises(osmapi.ChangesetClosedApiError) as cm:
+            self.api.NodeUpdate(test_node)
+
+        self.assertEquals(cm.exception.status, 409)
+        self.assertEquals(
+            cm.exception.payload,
+            "The changeset 2222 was closed at 2021-11-20 09:42:47 UTC."
+        )
+
+    def test_NodeUpdateConflict(self):
+        self._session_mock(auth=True, status=409)
+
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+
+        test_node = {
+            'id': 7676,
+            'lat': 47.287,
+            'lon': 8.765,
+            'tag': {
+                'amenity': 'place_of_worship',
+                'name': 'christian'
+            }
+        }
+
+        self.api.ChangesetCreate({
+            'comment': 'This is a test dataset'
+        })
+
+        with self.assertRaises(osmapi.VersionMismatchApiError) as cm:
+            self.api.NodeUpdate(test_node)
+
+        self.assertEquals(cm.exception.status, 409)
+        self.assertEquals(
+            cm.exception.payload,
+            "Version does not match the current database version of the element"
+        )
+
     def test_NodeDelete(self):
         self._session_mock(auth=True)
 
@@ -301,6 +363,18 @@ class TestOsmApiNode(osmapi_tests.TestOsmApi):
             }
         )
 
+    def test_NodeWaysNotExists(self):
+        self._session_mock()
+
+        result = self.api.NodeWays(404)
+
+        args, kwargs = self.api._session.request.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(args[1], f'{self.api_base}/api/0.6/node/404/ways')
+
+        self.assertEquals(len(result), 0)
+        self.assertIsInstance(result, list)
+
     def test_NodeRelations(self):
         self._session_mock()
 
@@ -329,6 +403,21 @@ class TestOsmApiNode(osmapi_tests.TestOsmApi):
             }
         )
 
+    def test_NodeRelationsUnusedElement(self):
+        self._session_mock()
+
+        result = self.api.NodeRelations(4295668179)
+
+        args, kwargs = self.api._session.request.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(
+            args[1],
+            self.api_base + '/api/0.6/node/4295668179/relations'
+        )
+
+        self.assertEquals(len(result), 0)
+        self.assertIsInstance(result, list)
+
     def test_NodesGet(self):
         self._session_mock()
 


=====================================
tests/osmapi_tests.py
=====================================
@@ -27,8 +27,7 @@ class TestOsmApi(unittest.TestCase):
         print(self._testMethodName)
         print(self.api)
 
-    def _session_mock(self, auth=False, filenames=None, status=200,
-                      reason=None):
+    def _session_mock(self, auth=False, filenames=None, status=200):
         if auth:
             self.api._username = 'testuser'
             self.api._password = 'testpassword'


=====================================
tests/relation_tests.py
=====================================
@@ -273,6 +273,21 @@ class TestOsmApiRelation(osmapi_tests.TestOsmApi):
             'Aargauischer Radroutennetz'
         )
 
+    def test_RelationRelationsUnusedElement(self):
+        self._session_mock()
+
+        result = self.api.RelationRelations(1532552)
+
+        args, kwargs = self.api._session.request.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(
+            args[1],
+            f'{self.api_base}/api/0.6/relation/1532552/relations'
+        )
+
+        self.assertEquals(len(result), 0)
+        self.assertIsInstance(result, list)
+
     def test_RelationFull(self):
         self._session_mock()
 


=====================================
tests/way_tests.py
=====================================
@@ -155,6 +155,39 @@ class TestOsmApiWay(osmapi_tests.TestOsmApi):
         self.assertEquals(result['nd'], test_way['nd'])
         self.assertEquals(result['tag'], test_way['tag'])
 
+    def test_WayUpdatePreconditionFailed(self):
+        self._session_mock(auth=True, status=412)
+
+        self.api.ChangesetCreate = mock.Mock(
+            return_value=1111
+        )
+        self.api._CurrentChangesetId = 1111
+
+        test_way = {
+            'id': 876,
+            'nd': [11949, 11950],
+            'tag': {
+                'highway': 'unclassified',
+                'name': 'Osmapi Street Update'
+            }
+        }
+
+        self.api.ChangesetCreate({
+            'comment': 'This is a test dataset'
+        })
+
+        with self.assertRaises(osmapi.PreconditionFailedApiError) as cm:
+            self.api.WayUpdate(test_way)
+
+        self.assertEquals(cm.exception.status, 412)
+        self.assertEquals(
+            cm.exception.payload,
+            (
+                "Way 876 requires the nodes with id in (11950), "
+                "which either do not exist, or are not visible."
+            )
+        )
+
     def test_WayDelete(self):
         self._session_mock(auth=True)
 
@@ -229,6 +262,21 @@ class TestOsmApiWay(osmapi_tests.TestOsmApi):
             }
         )
 
+    def test_WayRelationsUnusedElement(self):
+        self._session_mock()
+
+        result = self.api.WayRelations(4295032193)
+
+        args, kwargs = self.api._session.request.call_args
+        self.assertEquals(args[0], 'GET')
+        self.assertEquals(
+            args[1],
+            self.api_base + '/api/0.6/way/4295032193/relations'
+        )
+
+        self.assertEquals(len(result), 0)
+        self.assertIsInstance(result, list)
+
     def test_WayFull(self):
         self._session_mock()
 


=====================================
tox.ini
=====================================
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py34,py35,py36,py37
+envlist = py37,py38,py39
 [testenv]
 commands=nosetests --verbose
 deps =



View it on GitLab: https://salsa.debian.org/debian-gis-team/python-osmapi/-/commit/79856c6f3a32f4bdba94b2d6ebd8849e46423670

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-osmapi/-/commit/79856c6f3a32f4bdba94b2d6ebd8849e46423670
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20211123/f08222da/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list