diff -Nru python-osmapi-0.8.1/build.sh python-osmapi-1.1.0/build.sh --- python-osmapi-0.8.1/build.sh 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/build.sh 2017-10-11 21:35:45.000000000 +0000 @@ -14,10 +14,8 @@ # run tests nosetests --verbose --with-coverage -# generate docs (currently it's not possible to generate docs in Python 2.6) -if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then - pdoc --html --overwrite osmapi/OsmApi.py -fi +# generate the docs +pdoc --html --overwrite osmapi/OsmApi.py # setup a new virtualenv and try to install the lib virtualenv pyenv diff -Nru python-osmapi-0.8.1/CHANGELOG.md python-osmapi-1.1.0/CHANGELOG.md --- python-osmapi-0.8.1/CHANGELOG.md 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/CHANGELOG.md 2017-10-11 21:35:45.000000000 +0000 @@ -4,6 +4,34 @@ ## [Unreleased][unreleased] +## 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 +### 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 +### Fixed +- Make sure tests run offline + +## 1.0.0 - 2017-09-05 +### Added +- Officially support Python 3.5 and 3.6 + +### Removed +- osmapi does **not** support Python 2.6 anymore (it might work, it might not) + +### Changed +- **BC-Break:** raise an exception if the requested element is deleted (previoulsy `None` has been returned) + ## 0.8.1 - 2016-12-21 ### Fixed - Use setuptools instead of distutils in setup.py diff -Nru python-osmapi-0.8.1/debian/changelog python-osmapi-1.1.0/debian/changelog --- python-osmapi-0.8.1/debian/changelog 2016-12-22 06:18:47.000000000 +0000 +++ python-osmapi-1.1.0/debian/changelog 2017-10-12 06:03:12.000000000 +0000 @@ -1,3 +1,35 @@ +python-osmapi (1.1.0-1) unstable; urgency=medium + + * New upstream release. + * Bump Standards-Version to 4.1.1, no changes. + + -- Bas Couwenberg Thu, 12 Oct 2017 08:03:12 +0200 + +python-osmapi (1.0.2-1) unstable; urgency=medium + + * New upstream release. + * Drop python{,3}-httpretty build dependencies. + * Drop offline-tests.patch, fixed upstream. + + -- Bas Couwenberg Thu, 07 Sep 2017 16:04:15 +0200 + +python-osmapi (1.0.1-1) unstable; urgency=medium + + * New upstream release. + * Refresh patches. + + -- Bas Couwenberg Thu, 07 Sep 2017 10:00:46 +0200 + +python-osmapi (1.0.0-1) unstable; urgency=medium + + * New upstream release. + * Add python{,3}-httpretty to build dependencies. + * Bump Standards-Version to 4.0.0, no changes. + * Add autopkgtest to test installability. + * Add patch to skip test requiring network. + + -- Bas Couwenberg Tue, 05 Sep 2017 22:16:11 +0200 + python-osmapi (0.8.1-1) unstable; urgency=medium * New upstream release. diff -Nru python-osmapi-0.8.1/debian/control python-osmapi-1.1.0/debian/control --- python-osmapi-0.8.1/debian/control 2016-12-21 17:40:48.000000000 +0000 +++ python-osmapi-1.1.0/debian/control 2017-10-12 06:01:58.000000000 +0000 @@ -20,7 +20,7 @@ python3-requests, python-tox, python-xmltodict -Standards-Version: 3.9.8 +Standards-Version: 4.1.1 Vcs-Browser: https://anonscm.debian.org/cgit/pkg-grass/python-osmapi.git Vcs-Git: https://anonscm.debian.org/git/pkg-grass/python-osmapi.git Homepage: https://wiki.openstreetmap.org/wiki/Osmapi diff -Nru python-osmapi-0.8.1/debian/tests/control python-osmapi-1.1.0/debian/tests/control --- python-osmapi-0.8.1/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ python-osmapi-1.1.0/debian/tests/control 2017-09-05 20:48:30.000000000 +0000 @@ -0,0 +1,3 @@ +# Test installability +Depends: @ +Test-Command: /bin/true diff -Nru python-osmapi-0.8.1/.gitignore python-osmapi-1.1.0/.gitignore --- python-osmapi-0.8.1/.gitignore 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/.gitignore 2017-10-11 21:35:45.000000000 +0000 @@ -1,5 +1,6 @@ dist/ MANIFEST *.pyc +*.egg-info .coverage .tox diff -Nru python-osmapi-0.8.1/osmapi/__init__.py python-osmapi-1.1.0/osmapi/__init__.py --- python-osmapi-0.8.1/osmapi/__init__.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/osmapi/__init__.py 2017-10-11 21:35:45.000000000 +0000 @@ -1,5 +1,5 @@ from __future__ import (absolute_import, print_function, unicode_literals) -__version__ = '0.8.1' +__version__ = '1.1.0' from .OsmApi import * # noqa diff -Nru python-osmapi-0.8.1/osmapi/OsmApi.py python-osmapi-1.1.0/osmapi/OsmApi.py --- python-osmapi-0.8.1/osmapi/OsmApi.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/osmapi/OsmApi.py 2017-10-11 21:35:45.000000000 +0000 @@ -29,6 +29,7 @@ from __future__ import (absolute_import, print_function, unicode_literals) import xml.dom.minidom +import xml.parsers.expat import time import sys import urllib @@ -84,6 +85,12 @@ 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 @@ -122,6 +129,20 @@ 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 OsmApi: """ Main class of osmapi, instanciate this class to use osmapi @@ -228,8 +249,12 @@ self._session = self._get_http_session() def __del__(self): - if self._changesetauto: - self._changesetautoflush(True) + try: + if self._changesetauto: + self._changesetautoflush(True) + except ResponseEmptyApiError: + pass + return None ################################################## @@ -273,9 +298,8 @@ """ uri = "/api/capabilities" data = self._get(uri) - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("api")[0] + + data = self._OsmResponseToDom(data, tag="api", single=True) result = {} for elem in data.childNodes: if elem.nodeType != elem.ELEMENT_NODE: @@ -312,16 +336,15 @@ If `NodeVersion` is supplied, this specific version is returned, otherwise the latest version is returned. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ uri = "/api/0.6/node/%s" % (NodeId) if NodeVersion != -1: uri += "/%s" % (NodeVersion) data = self._get(uri) - if not data: - return data - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("node")[0] + data = self._OsmResponseToDom(data, tag="node", single=True) return self._DomParseNode(data) def NodeCreate(self, NodeData): @@ -436,6 +459,9 @@ If there is already an open changeset, `OsmApi.ChangesetAlreadyOpenError` is raised. + + If the requested element has already been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ return self._do("delete", "node", NodeData) @@ -454,11 +480,10 @@ """ uri = "/api/0.6/node/%s/history" % NodeId data = self._get(uri) - data = xml.dom.minidom.parseString(data) + nodes = self._OsmResponseToDom(data, tag="node") result = {} - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("node"): - data = self._DomParseNode(data) + for node in nodes: + data = self._DomParseNode(node) result[data["version"]] = data return result @@ -487,11 +512,10 @@ """ uri = "/api/0.6/node/%d/ways" % NodeId data = self._get(uri) - data = xml.dom.minidom.parseString(data) + ways = self._OsmResponseToDom(data, tag="way") result = [] - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("way"): - data = self._DomParseWay(data) + for way in ways: + data = self._DomParseWay(way) result.append(data) return result @@ -529,11 +553,10 @@ """ uri = "/api/0.6/node/%d/relations" % NodeId data = self._get(uri) - data = xml.dom.minidom.parseString(data) + relations = self._OsmResponseToDom(data, tag="relation") result = [] - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("relation"): - data = self._DomParseRelation(data) + for relation in relations: + data = self._DomParseRelation(relation) result.append(data) return result @@ -555,11 +578,10 @@ node_list = ",".join([str(x) for x in NodeIdList]) uri = "/api/0.6/nodes?nodes=%s" % node_list data = self._get(uri) - data = xml.dom.minidom.parseString(data) + nodes = self._OsmResponseToDom(data, tag="node") result = {} - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("node"): - data = self._DomParseNode(data) + for node in nodes: + data = self._DomParseNode(node) result[data["id"]] = data return result @@ -586,17 +608,16 @@ If `WayVersion` is supplied, this specific version is returned, otherwise the latest version is returned. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ uri = "/api/0.6/way/%s" % (WayId) if WayVersion != -1: uri += "/%s" % (WayVersion) data = self._get(uri) - if not data: - return data - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("way")[0] - return self._DomParseWay(data) + way = self._OsmResponseToDom(data, tag="way", single=True) + return self._DomParseWay(way) def WayCreate(self, WayData): """ @@ -707,6 +728,9 @@ If there is already an open changeset, `OsmApi.ChangesetAlreadyOpenError` is raised. + + If the requested element has already been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ return self._do("delete", "way", WayData) @@ -725,11 +749,10 @@ """ uri = "/api/0.6/way/%s/history" % (WayId) data = self._get(uri) - data = xml.dom.minidom.parseString(data) + ways = self._OsmResponseToDom(data, tag="way") result = {} - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("way"): - data = self._DomParseWay(data) + for way in ways: + data = self._DomParseWay(way) result[data["version"]] = data return result @@ -767,11 +790,10 @@ """ uri = "/api/0.6/way/%d/relations" % WayId data = self._get(uri) - data = xml.dom.minidom.parseString(data) + relations = self._OsmResponseToDom(data, tag="relation") result = [] - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("relation"): - data = self._DomParseRelation(data) + for relation in relations: + data = self._DomParseRelation(relation) result.append(data) return result @@ -789,6 +811,9 @@ ] The `WayId` is a unique identifier for a way. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ uri = "/api/0.6/way/%s/full" % (WayId) data = self._get(uri) @@ -811,11 +836,10 @@ way_list = ",".join([str(x) for x in WayIdList]) uri = "/api/0.6/ways?ways=%s" % way_list data = self._get(uri) - data = xml.dom.minidom.parseString(data) + ways = self._OsmResponseToDom(data, tag="way") result = {} - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("way"): - data = self._DomParseWay(data) + for way in ways: + data = self._DomParseWay(way) result[data["id"]] = data return result @@ -851,17 +875,16 @@ If `RelationVersion` is supplied, this specific version is returned, otherwise the latest version is returned. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ uri = "/api/0.6/relation/%s" % (RelationId) if RelationVersion != -1: uri += "/%s" % (RelationVersion) data = self._get(uri) - if not data: - return data - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("relation")[0] - return self._DomParseRelation(data) + relation = self._OsmResponseToDom(data, tag="relation", single=True) + return self._DomParseRelation(relation) def RelationCreate(self, RelationData): """ @@ -999,6 +1022,9 @@ If there is already an open changeset, `OsmApi.ChangesetAlreadyOpenError` is raised. + + If the requested element has already been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ return self._do("delete", "relation", RelationData) @@ -1017,11 +1043,10 @@ """ uri = "/api/0.6/relation/%s/history" % (RelationId) data = self._get(uri) - data = xml.dom.minidom.parseString(data) + relations = self._OsmResponseToDom(data, tag="relation") result = {} - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("relation"): - data = self._DomParseRelation(data) + for relation in relations: + data = self._DomParseRelation(relation) result[data["version"]] = data return result @@ -1060,11 +1085,10 @@ """ uri = "/api/0.6/relation/%d/relations" % RelationId data = self._get(uri) - data = xml.dom.minidom.parseString(data) + relations = self._OsmResponseToDom(data, tag="relation") result = [] - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("relation"): - data = self._DomParseRelation(data) + for relation in relations: + data = self._DomParseRelation(relation) result.append(data) return result @@ -1088,6 +1112,9 @@ If you don't need all levels, use `OsmApi.RelationFull` instead, which return only 2 levels. + + If any relation (on any level) has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ data = [] todo = [RelationId] @@ -1122,6 +1149,9 @@ The `RelationId` is a unique identifier for a way. If you need all levels, use `OsmApi.RelationFullRecur`. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ uri = "/api/0.6/relation/%s/full" % (RelationId) data = self._get(uri) @@ -1145,11 +1175,10 @@ relation_list = ",".join([str(x) for x in RelationIdList]) uri = "/api/0.6/relations?relations=%s" % relation_list data = self._get(uri) - data = xml.dom.minidom.parseString(data) + relations = self._OsmResponseToDom(data, tag="relation") result = {} - osm_data = data.getElementsByTagName("osm")[0] - for data in osm_data.getElementsByTagName("relation"): - data = self._DomParseRelation(data) + for relation in relations: + data = self._DomParseRelation(relation) result[data["id"]] = data return result @@ -1187,10 +1216,8 @@ if (include_discussion): path += "?include_discussion=true" data = self._get(path) - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("changeset")[0] - return self._DomParseChangeset(data) + changeset = self._OsmResponseToDom(data, tag="changeset", single=True) + return self._DomParseChangeset(changeset) def ChangesetUpdate(self, ChangesetTags={}): """ @@ -1208,7 +1235,8 @@ ChangesetTags["created_by"] = self._created_by self._put( "/api/0.6/changeset/%s" % (self._CurrentChangesetId), - self._XmlBuild("changeset", {"tag": ChangesetTags}) + self._XmlBuild("changeset", {"tag": ChangesetTags}), + return_value=False ) return self._CurrentChangesetId @@ -1253,7 +1281,8 @@ 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 @@ -1293,9 +1322,15 @@ "/api/0.6/changeset/%s/upload" % (self._CurrentChangesetId), data.encode("utf-8") ) - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("diffResult")[0] - data = [x for x in data.childNodes if x.nodeType == x.ELEMENT_NODE] + try: + data = xml.dom.minidom.parseString(data) + data = data.getElementsByTagName("diffResult")[0] + data = [x for x in data.childNodes if x.nodeType == x.ELEMENT_NODE] + except (xml.parsers.expat.ExpatError, IndexError) as e: + raise XmlResponseInvalidError( + "The XML response from the OSM API is invalid: %r" % e + ) + for i in range(len(ChangesData)): if ChangesData[i]["action"] == "delete": ChangesData[i]["data"].pop("version") @@ -1379,11 +1414,9 @@ uri += "?" + urllib.urlencode(params) data = self._get(uri) - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("changeset") + changesets = self._OsmResponseToDom(data, tag="changeset") result = {} - for curChangeset in data: + for curChangeset in changesets: tmpCS = self._DomParseChangeset(curChangeset) result[tmpCS["id"]] = tmpCS return result @@ -1421,10 +1454,8 @@ "/api/0.6/changeset/%s/comment" % (ChangesetId), params ) - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("changeset")[0] - return self._DomParseChangeset(data) + changeset = self._OsmResponseToDom(data, tag="changeset", single=True) + return self._DomParseChangeset(changeset) def ChangesetSubscribe(self, ChangesetId): """ @@ -1463,10 +1494,8 @@ raise AlreadySubscribedApiError(e.status, e.reason, e.payload) else: raise - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("changeset")[0] - return self._DomParseChangeset(data) + changeset = self._OsmResponseToDom(data, tag="changeset", single=True) + return self._DomParseChangeset(changeset) def ChangesetUnsubscribe(self, ChangesetId): """ @@ -1505,10 +1534,8 @@ raise NotSubscribedApiError(e.status, e.reason, e.payload) else: raise - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] - data = data.getElementsByTagName("changeset")[0] - return self._DomParseChangeset(data) + changeset = self._OsmResponseToDom(data, tag="changeset", single=True) + return self._DomParseChangeset(changeset) ################################################## # Notes # @@ -1576,12 +1603,8 @@ """ uri = "/api/0.6/notes/%s" % (id) data = self._get(uri) - data = xml.dom.minidom.parseString(data) - osm_data = data.getElementsByTagName("osm")[0] - - noteElement = osm_data.getElementsByTagName("note")[0] - note = self._DomParseNote(noteElement) - return note + noteElement = self._OsmResponseToDom(data, tag="note", single=True) + return self._DomParseNote(noteElement) def NoteCreate(self, NoteData): """ @@ -1622,6 +1645,9 @@ If no authentication information are provided, `OsmApi.UsernamePasswordMissingError` is raised. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. """ path = "/api/0.6/notes/%s/reopen" % NoteId return self._NoteAction(path, comment, optionalAuth=False) @@ -1661,13 +1687,8 @@ result = self._post(uri, None, optionalAuth=optionalAuth) # parse the result - data = xml.dom.minidom.parseString(result) - osm_data = data.getElementsByTagName("osm")[0] - - noteElement = osm_data.getElementsByTagName("note")[0] - note = self._DomParseNote(noteElement) - - return note + noteElement = self._OsmResponseToDom(result, tag="note", single=True) + return self._DomParseNote(noteElement) ################################################## # Other # @@ -1708,8 +1729,14 @@ data: {} } """ - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osm")[0] + try: + data = xml.dom.minidom.parseString(data) + data = data.getElementsByTagName("osm")[0] + except (xml.parsers.expat.ExpatError, IndexError) as e: + raise XmlResponseInvalidError( + "The XML response from the OSM API is invalid: %r" % e + ) + result = [] for elem in data.childNodes: if elem.nodeName == "node": @@ -1742,8 +1769,14 @@ data: {} } """ - data = xml.dom.minidom.parseString(data) - data = data.getElementsByTagName("osmChange")[0] + try: + data = xml.dom.minidom.parseString(data) + data = data.getElementsByTagName("osmChange")[0] + except (xml.parsers.expat.ExpatError, IndexError) as e: + raise XmlResponseInvalidError( + "The XML response from the OSM API is invalid: %r" % e + ) + result = [] for action in data.childNodes: if action.nodeName == "#text": @@ -1790,11 +1823,9 @@ { ... } ] """ - data = xml.dom.minidom.parseString(data) + noteElements = self._OsmResponseToDom(data, tag="note") result = [] - osm_data = data.getElementsByTagName("osm")[0] - - for noteElement in osm_data.getElementsByTagName("note"): + for noteElement in noteElements: note = self._DomParseNote(noteElement) result.append(note) return result @@ -1885,7 +1916,7 @@ self._changesetautocpt = 0 return None - def _http_request(self, method, path, auth, send): # noqa + def _http_request(self, method, path, auth, send, return_value=True): # noqa """ Returns the response generated by an HTTP request. @@ -1898,6 +1929,17 @@ be preformed on this request. `send` contains additional data that might be sent in a request. + `return_value` indicates wheter this request should return + any data or not. + + If the username or password is missing, + `OsmApi.UsernamePasswordMissingError` is raised. + + If the requested element has been deleted, + `OsmApi.ElementDeletedApiError` is raised. + + If the response status code indicates an error, + `OsmApi.ApiError` is raised. """ if self._debug: error_msg = ( @@ -1919,10 +1961,21 @@ response = self._session.request(method, path, auth=user_pass, data=send) if response.status_code != 200: - if response.status_code == 410: - return None payload = response.content.strip() + if response.status_code == 410: + raise ElementDeletedApiError( + response.status_code, + response.reason, + payload + ) raise ApiError(response.status_code, response.reason, payload) + if return_value and not response.content: + raise ResponseEmptyApiError( + response.status_code, + response.reason, + '' + ) + if self._debug: error_msg = ( "%s %s %s" @@ -1931,12 +1984,18 @@ print(error_msg, file=sys.stderr) return response.content - def _http(self, cmd, path, auth, send): # noqa + def _http(self, cmd, path, auth, send, return_value=True): # noqa i = 0 while True: i += 1 try: - return self._http_request(cmd, path, auth, send) + return self._http_request( + cmd, + path, + auth, + send, + return_value=return_value + ) except ApiError as e: if e.status >= 500: if i == self.MAX_RETRY_LIMIT: @@ -1974,8 +2033,8 @@ def _get(self, path): return self._http('GET', path, False, None) - def _put(self, path, data): - return self._http('PUT', path, True, data) + def _put(self, path, data, return_value=True): + return self._http('PUT', path, True, data, return_value=return_value) def _post(self, path, data, optionalAuth=False): auth = True @@ -1991,6 +2050,24 @@ # Internal dom function # ################################################## + def _OsmResponseToDom(self, response, tag, single=False): + """ + Returns the (sub-) DOM parsed from an OSM response + """ + try: + dom = xml.dom.minidom.parseString(response) + 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: + raise XmlResponseInvalidError( + "The XML response from the OSM API is invalid: %r" % e + ) + + if single: + return first_element + return all_data + def _DomGetAttributes(self, DomElement): # noqa """ Returns a formated dictionnary of attributes of a DomElement. diff -Nru python-osmapi-0.8.1/README.md python-osmapi-1.1.0/README.md --- python-osmapi-0.8.1/README.md 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/README.md 2017-10-11 21:35:45.000000000 +0000 @@ -28,6 +28,8 @@ ## Examples +To test this library, please create an account on the [development server of OpenStreetMap (https://api06.dev.openstreetmap.org)](https://api06.dev.openstreetmap.org). + ### Read from OpenStreetMap ```python @@ -45,18 +47,18 @@ ```python import osmapi -api = osmapi.OsmApi(api="api06.dev.openstreetmap.org", username = "you", password = "***") +api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", username = "you", password = "***") api = osmapi.OsmApi(username = "you", passwordfile = "/etc/mypasswords") -api = osmapi.OsmApi(passwordfile = "/etc/mypasswords") # username will be first line username +api = osmapi.OsmApi(passwordfile = "/etc/mypasswords") # if only the passwordfile is specified, the credentials on the first line of the file will be used ``` -Note: The password file should have the format _user:password_ +Note: Each line in the password file should have the format _user:password_ ### Write to OpenStreetMap ```python import osmapi -api = osmapi.OsmApi(username = u"metaodi", password = u"*******") +api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org", username = u"metaodi", password = u"*******") api.ChangesetCreate({u"comment": u"My first test"}) print api.NodeCreate({u"lon":1, u"lat":1, u"tag": {}}) # {u'changeset': 532907, u'lon': 1, u'version': 1, u'lat': 1, u'tag': {}, u'id': 164684} @@ -86,10 +88,21 @@ nosetests --verbose -By using tox you can even run the tests against different versions of python (2.6, 2.7, 3.2 and 3.3): +By using tox you can even run the tests against different versions of python (2.7, 3.3, 3.4, 3.5 and 3.6): tox +## Release + +To create a new release, follow these steps (please respect [Semantic Versioning](http://semver.org/)): + +1. Adapt the version number in `osmapi/__init__.py` +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 + ## Attribution This project was orginally developed by Etienne Chové. diff -Nru python-osmapi-0.8.1/setup.py python-osmapi-1.1.0/setup.py --- python-osmapi-0.8.1/setup.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/setup.py 2017-10-11 21:35:45.000000000 +0000 @@ -42,10 +42,11 @@ 'Topic :: Software Development :: Libraries', 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', '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', ], ) diff -Nru python-osmapi-0.8.1/test-requirements.txt python-osmapi-1.1.0/test-requirements.txt --- python-osmapi-0.8.1/test-requirements.txt 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/test-requirements.txt 2017-10-11 21:35:45.000000000 +0000 @@ -3,7 +3,7 @@ flake8==3.0.4; python_version >= '2.7' flake8==2.1.0; python_version == '2.6' nose==1.3.0 -tox==1.7.1 +tox==2.8.1 coverage==3.7.1 coveralls==0.4.1 mock==1.0.1 diff -Nru python-osmapi-0.8.1/tests/changeset_tests.py python-osmapi-1.1.0/tests/changeset_tests.py --- python-osmapi-0.8.1/tests/changeset_tests.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/changeset_tests.py 2017-10-11 21:35:45.000000000 +0000 @@ -92,10 +92,10 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b' \n' b' \n' - b' \n' + b' \n' b' \n' b'\n' ) @@ -125,7 +125,7 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b' \n' b' \n' b' \n' @@ -163,10 +163,10 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b' \n' b' \n' - b' \n' + b' \n' b' \n' b'\n' ) @@ -190,7 +190,7 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b' \n' b' \n' b' \n' @@ -276,7 +276,7 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b'\n' b' \n' @@ -350,7 +350,7 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b'\n' b' \n' @@ -434,7 +434,7 @@ xmltosorteddict(kwargs['data']), xmltosorteddict( b'\n' - b'\n' + b'\n' b'\n' b' \n' @@ -458,6 +458,46 @@ self.assertEquals(data['id'], 676) self.assertNotIn('version', data) + def test_ChangesetUpload_invalid_response(self): + self._session_mock(auth=True) + + # setup mock + self.api.ChangesetCreate = mock.Mock( + return_value=4444 + ) + self.api._CurrentChangesetId = 4444 + + changesdata = [ + { + 'type': 'relation', + 'action': 'delete', + 'data': { + 'id': 676, + 'version': 2, + 'member': [ + { + 'ref': 4799, + 'role': 'outer', + 'type': 'way' + }, + { + 'ref': 9391, + 'role': 'outer', + 'type': 'way' + }, + ], + 'tag': { + 'admin_level': '9', + 'boundary': 'administrative', + 'type': 'multipolygon' + } + } + } + ] + + with self.assertRaises(osmapi.XmlResponseInvalidError): + self.api.ChangesetUpload(changesdata) + def test_ChangesetDownload(self): self._session_mock() @@ -491,6 +531,11 @@ } ) + def test_ChangesetDownload_invalid_response(self): + self._session_mock() + with self.assertRaises(osmapi.XmlResponseInvalidError): + self.api.ChangesetDownload(23123) + def test_ChangesetDownloadContainingUnicode(self): self._session_mock() diff -Nru python-osmapi-0.8.1/tests/fixtures/passwordfile.txt python-osmapi-1.1.0/tests/fixtures/passwordfile.txt --- python-osmapi-0.8.1/tests/fixtures/passwordfile.txt 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/fixtures/passwordfile.txt 2017-10-11 21:35:45.000000000 +0000 @@ -1 +1,2 @@ testosm:testpass +testuser:testuserpass diff -Nru python-osmapi-0.8.1/tests/fixtures/test_ChangesetDownload_invalid_response.xml python-osmapi-1.1.0/tests/fixtures/test_ChangesetDownload_invalid_response.xml --- python-osmapi-0.8.1/tests/fixtures/test_ChangesetDownload_invalid_response.xml 1970-01-01 00:00:00.000000000 +0000 +++ python-osmapi-1.1.0/tests/fixtures/test_ChangesetDownload_invalid_response.xml 2017-10-11 21:35:45.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-osmapi-0.8.1/tests/fixtures/test_ChangesetUpload_invalid_response.xml python-osmapi-1.1.0/tests/fixtures/test_ChangesetUpload_invalid_response.xml --- python-osmapi-0.8.1/tests/fixtures/test_ChangesetUpload_invalid_response.xml 1970-01-01 00:00:00.000000000 +0000 +++ python-osmapi-1.1.0/tests/fixtures/test_ChangesetUpload_invalid_response.xml 2017-10-11 21:35:45.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-osmapi-0.8.1/tests/fixtures/test_NodeGet_invalid_response.xml python-osmapi-1.1.0/tests/fixtures/test_NodeGet_invalid_response.xml --- python-osmapi-0.8.1/tests/fixtures/test_NodeGet_invalid_response.xml 1970-01-01 00:00:00.000000000 +0000 +++ python-osmapi-1.1.0/tests/fixtures/test_NodeGet_invalid_response.xml 2017-10-11 21:35:45.000000000 +0000 @@ -0,0 +1,3 @@ + + + diff -Nru python-osmapi-0.8.1/tests/fixtures/test_NoteGet_invalid_xml.xml python-osmapi-1.1.0/tests/fixtures/test_NoteGet_invalid_xml.xml --- python-osmapi-0.8.1/tests/fixtures/test_NoteGet_invalid_xml.xml 1970-01-01 00:00:00.000000000 +0000 +++ python-osmapi-1.1.0/tests/fixtures/test_NoteGet_invalid_xml.xml 2017-10-11 21:35:45.000000000 +0000 @@ -0,0 +1,13 @@ + + + + 1111 + http://api.openstreetmap.org/api/0.6/notes/1111 + http://api.openstreetmap.org/api/0.6/notes/1111/reopen + 2013-05-01 20:58:21 UTC + closed + 2013-08-21 16:43:26 UTC + + + + diff -Nru python-osmapi-0.8.1/tests/fixtures/test_WayFull_invalid_response.xml python-osmapi-1.1.0/tests/fixtures/test_WayFull_invalid_response.xml --- python-osmapi-0.8.1/tests/fixtures/test_WayFull_invalid_response.xml 1970-01-01 00:00:00.000000000 +0000 +++ python-osmapi-1.1.0/tests/fixtures/test_WayFull_invalid_response.xml 2017-10-11 21:35:45.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru python-osmapi-0.8.1/tests/helper_tests.py python-osmapi-1.1.0/tests/helper_tests.py --- python-osmapi-0.8.1/tests/helper_tests.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/helper_tests.py 2017-10-11 21:35:45.000000000 +0000 @@ -26,7 +26,7 @@ self.api._username = 'testuser' self.api._password = 'testpassword' - def test_passwordfile(self): + def test_passwordfile_only(self): path = os.path.join( __location__, 'fixtures', @@ -36,6 +36,16 @@ self.assertEquals('testosm', my_api._username) self.assertEquals('testpass', my_api._password) + def test_passwordfile_with_user(self): + path = os.path.join( + __location__, + 'fixtures', + 'passwordfile.txt' + ) + my_api = osmapi.OsmApi(username='testuser', passwordfile=path) + self.assertEquals('testuser', my_api._username) + self.assertEquals('testuserpass', my_api._password) + def test_http_request_get(self): response = self.api._http_request( 'GET', @@ -101,19 +111,16 @@ def test_http_request_410_response(self): self.setupMock(410) - response = self.api._http_request( - 'GET', - '/api/0.6/test410', - False, - None - ) - self.api._session.request.assert_called_with( - 'GET', - self.api_base + '/api/0.6/test410', - auth=None, - data=None - ) - self.assertIsNone(response, "test response") + with self.assertRaises(osmapi.ElementDeletedApiError) as cm: + self.api._http_request( + 'GET', + '/api/0.6/test410', + False, + None + ) + self.assertEquals(cm.exception.status, 410) + self.assertEquals(cm.exception.reason, "test reason") + self.assertEquals(cm.exception.payload, "test response") def test_http_request_500_response(self): self.setupMock(500) diff -Nru python-osmapi-0.8.1/tests/node_tests.py python-osmapi-1.1.0/tests/node_tests.py --- python-osmapi-0.8.1/tests/node_tests.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/node_tests.py 2017-10-11 21:35:45.000000000 +0000 @@ -56,6 +56,12 @@ }, }) + def test_NodeGet_invalid_response(self): + self._session_mock() + + with self.assertRaises(osmapi.XmlResponseInvalidError): + self.api.NodeGet(987) + def test_NodeCreate_changesetauto(self): # setup mock self.api = osmapi.OsmApi( diff -Nru python-osmapi-0.8.1/tests/notes_tests.py python-osmapi-1.1.0/tests/notes_tests.py --- python-osmapi-0.8.1/tests/notes_tests.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/notes_tests.py 2017-10-11 21:35:45.000000000 +0000 @@ -1,6 +1,7 @@ from __future__ import (unicode_literals, absolute_import) from . import osmapi_tests from datetime import datetime +import osmapi try: import urlparse @@ -98,6 +99,12 @@ ] }) + def test_NoteGet_invalid_xml(self): + self._session_mock() + + with self.assertRaises(osmapi.XmlResponseInvalidError): + self.api.NoteGet(1111) + def test_NoteCreate(self): self._session_mock(auth=True) diff -Nru python-osmapi-0.8.1/tests/relation_tests.py python-osmapi-1.1.0/tests/relation_tests.py --- python-osmapi-0.8.1/tests/relation_tests.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/relation_tests.py 2017-10-11 21:35:45.000000000 +0000 @@ -310,3 +310,10 @@ self.assertEquals(result[1532552]['id'], 1532552) self.assertEquals(result[1532552]['visible'], True) self.assertEquals(result[1532552]['tag']['route'], 'bicycle') + + def test_RelationFull_with_deleted_relation(self): + self._session_mock(filenames=[], status=410) + + with self.assertRaises(osmapi.ElementDeletedApiError) as context: + self.api.RelationFull(2911456) + self.assertEquals(410, context.exception.status) diff -Nru python-osmapi-0.8.1/tests/way_tests.py python-osmapi-1.1.0/tests/way_tests.py --- python-osmapi-0.8.1/tests/way_tests.py 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tests/way_tests.py 2017-10-11 21:35:45.000000000 +0000 @@ -65,13 +65,8 @@ def test_WayGet_nodata(self): self._session_mock() - result = self.api.WayGet(321) - - args, kwargs = self.api._session.request.call_args - self.assertEquals(args[0], 'GET') - self.assertEquals(args[1], self.api_base + '/api/0.6/way/321') - - self.assertEquals(result, '') + with self.assertRaises(osmapi.ResponseEmptyApiError): + self.api.WayGet(321) def test_WayCreate(self): self._session_mock(auth=True) @@ -251,6 +246,12 @@ self.assertEquals(result[16]['data']['changeset'], 298) self.assertEquals(result[16]['type'], 'way') + def test_WayFull_invalid_response(self): + self._session_mock() + + with self.assertRaises(osmapi.XmlResponseInvalidError): + self.api.WayFull(321) + def test_WaysGet(self): self._session_mock() diff -Nru python-osmapi-0.8.1/tox.ini python-osmapi-1.1.0/tox.ini --- python-osmapi-0.8.1/tox.ini 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/tox.ini 2017-10-11 21:35:45.000000000 +0000 @@ -1,11 +1,7 @@ [tox] -envlist = py26,py27,py32,py33,py34 +envlist = py27,py33,py34,py35,py36 [testenv] commands=nosetests --verbose deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -[testenv:py26] -deps = - unittest2 - {[testenv]deps} diff -Nru python-osmapi-0.8.1/.travis.yml python-osmapi-1.1.0/.travis.yml --- python-osmapi-0.8.1/.travis.yml 2016-12-21 22:00:01.000000000 +0000 +++ python-osmapi-1.1.0/.travis.yml 2017-10-11 21:35:45.000000000 +0000 @@ -1,19 +1,17 @@ language: python python: -- '2.6' - '2.7' - '3.3' - '3.4' +- '3.5' +- '3.6' before_install: - sudo apt-get update -qq - sudo apt-get install -qq pandoc install: -- if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then - pip install unittest2; - fi - pip install -r requirements.txt - pip install -r test-requirements.txt - pip install .