From f9143ff0a84e3e28f31bd040475ab5dcaa60c011 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 10 Mar 2024 12:06:35 +0100 Subject: [PATCH 01/50] oAuth2 access token support --- CHANGELOG.md | 7 ++++ src/osm_easy_api/api/api.py | 39 ++++++------------- src/osm_easy_api/api/endpoints/changeset.py | 11 ++---- .../api/endpoints/changeset_discussion.py | 10 ++--- src/osm_easy_api/api/endpoints/elements.py | 15 ++----- src/osm_easy_api/api/endpoints/gpx.py | 2 +- src/osm_easy_api/api/endpoints/misc.py | 1 - src/osm_easy_api/api/endpoints/notes.py | 8 ---- src/osm_easy_api/api/endpoints/user.py | 10 ++--- tests/api/test_api.py | 23 ++++++----- 10 files changed, 49 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d6e3e..1436e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Support for `oAuth2`: `access_token` parameter in `Api` class constructor. + +### Removed +- Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. + ## [2.2.0] ### Added - Exception for `410` status code in notes endpoint. diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index 2cff1b8..40dbcc4 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -1,5 +1,4 @@ import requests -from requests.auth import HTTPBasicAuth from xml.etree import ElementTree from enum import Enum @@ -13,11 +12,6 @@ class Api(): """Class used to communicate with API.""" - class _Requirement(Enum): - YES = 0, - NO = 1, - OPTIONAL = 2 - class _RequestMethods(Enum): GET = 0, PUT = 1, @@ -27,7 +21,7 @@ class _RequestMethods(Enum): def __str__(self): return self.name - def __init__(self, url: str = "https://master.apis.dev.openstreetmap.org", username: str | None = None, password: str | None = None, user_agent: str | None = None): + def __init__(self, url: str = "https://master.apis.dev.openstreetmap.org", access_token: str | None = None, user_agent: str | None = None): self._url = URLs(url) self.misc = Misc_Container(self) self.changeset = Changeset_Container(self) @@ -36,26 +30,15 @@ def __init__(self, url: str = "https://master.apis.dev.openstreetmap.org", usern self.user = User_Container(self) self.notes = Notes_Container(self) - if username and password: - self._auth = HTTPBasicAuth(username.encode('utf-8'), password.encode('utf-8')) - else: - self._auth = None + self._headers = {} + if access_token: + self._headers.update({"Authorization": "Bearer {}".format(access_token)}) if user_agent: - self._user_agent = user_agent + self._headers.update({"User-Agent": user_agent}) - def _request(self, method: _RequestMethods, url: str, auth_requirement: _Requirement = _Requirement.OPTIONAL, stream: bool = False, auto_status_code_handling: bool = True, body = None) -> "Response": - headers = {} - if hasattr(self, "_user_agent"): - headers.update({"User-Agent": self._user_agent}) - match auth_requirement: - case self._Requirement.YES: - if not self._auth: raise ValueError("No credentials provided during class initialization!") - response = requests.request(str(method), url, stream=stream, auth=self._auth, data=body.encode('utf-8') if body else None, headers=headers) - case self._Requirement.OPTIONAL: - response = requests.request(str(method), url, stream=stream, auth=self._auth, data=body.encode('utf-8') if body else None, headers=headers) - case self._Requirement.NO: - response = requests.request(str(method), url, stream=stream, data=body.encode('utf-8') if body else None, headers=headers) + def _request(self, method: _RequestMethods, url: str, stream: bool = False, auto_status_code_handling: bool = True, body = None) -> "Response": + response = requests.request(str(method), url, stream=stream, data=body.encode('utf-8') if body else None, headers=self._headers) if auto_status_code_handling: assert response.status_code == 200, f"Invalid (and unexpected) response code {response.status_code} for {url}" return response @@ -65,16 +48,16 @@ def _raw_stream_parser(xml_raw_stream: "HTTPResponse") -> Generator[ElementTree. for event, element in iterator: yield element - def _get_generator(self, url: str, auth_requirement: _Requirement = _Requirement.OPTIONAL, auto_status_code_handling: bool = True) -> Generator[ElementTree.Element, None, None] | Tuple[int, Generator[ElementTree.Element, None, None]]: - response = self._request(self._RequestMethods.GET, url, auth_requirement, auto_status_code_handling=auto_status_code_handling, stream=True) + def _get_generator(self, url: str, auto_status_code_handling: bool = True) -> Generator[ElementTree.Element, None, None] | Tuple[int, Generator[ElementTree.Element, None, None]]: + response = self._request(self._RequestMethods.GET, url, auto_status_code_handling=auto_status_code_handling, stream=True) response.raw.decode_content = True if auto_status_code_handling: return self._raw_stream_parser(response.raw) else: return (response.status_code, self._raw_stream_parser(response.raw)) - def _post_generator(self, url: str, auth_requirement: _Requirement = _Requirement.OPTIONAL, auto_status_code_handling: bool = True) -> Generator[ElementTree.Element, None, None] | Tuple[int, Generator[ElementTree.Element, None, None]]: - response = self._request(self._RequestMethods.POST, url, auth_requirement, auto_status_code_handling=auto_status_code_handling, stream=True) + def _post_generator(self, url: str, auto_status_code_handling: bool = True) -> Generator[ElementTree.Element, None, None] | Tuple[int, Generator[ElementTree.Element, None, None]]: + response = self._request(self._RequestMethods.POST, url, auto_status_code_handling=auto_status_code_handling, stream=True) response.raw.decode_content = True if auto_status_code_handling: return self._raw_stream_parser(response.raw) diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 760f349..54f6d9b 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -83,7 +83,7 @@ def create(self, comment: str, tags: Tags | None = None) -> int: xml_str = root.toprettyxml(indent="\t") response = self.outer._request(self.outer._RequestMethods.PUT, - self.outer._url.changeset["create"], self.outer._Requirement.YES, body=xml_str) + self.outer._url.changeset["create"], body=xml_str) return int(response.text) def get(self, id: int, include_discussion: bool = False) -> Changeset: @@ -103,7 +103,6 @@ def get(self, id: int, include_discussion: bool = False) -> Changeset: param = f"{id}?include_discussion={include_discussion_text}" status_code, generator = self.outer._get_generator( url=join_url(self.outer._url.changeset["get"], param), - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -161,7 +160,6 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ status_code, generator = self.outer._get_generator( url=join_url(self.outer._url.changeset["get_query"], param), - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -210,7 +208,7 @@ def update(self, id: int, comment: str | None = None, tags: Tags | None = None) xml_str = root.toprettyxml(indent="\t") response = self.outer._request(self.outer._RequestMethods.PUT, - self.outer._url.changeset["update"].format(id=id), self.outer._Requirement.YES, body=xml_str, stream=True, auto_status_code_handling = False) + self.outer._url.changeset["update"].format(id=id), body=xml_str, stream=True, auto_status_code_handling = False) match response.status_code: case 200: pass @@ -231,7 +229,7 @@ def close(self, id: int) -> None: exceptions.IdNotFoundError: There is no changeset with given ID. exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: The changeset was already closer or you are not the author. """ - response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.changeset["close"].format(id = id), self.outer._Requirement.YES, auto_status_code_handling = False) + response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.changeset["close"].format(id = id), auto_status_code_handling = False) match response.status_code: case 200: pass case 404: raise exceptions.IdNotFoundError() @@ -250,7 +248,7 @@ def download(self, id: int) -> Generator[Tuple['Action', 'Node | Way | Relation' Yields: Generator: Diff generator like in 'diff' module. """ - stream = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.changeset["download"].format(id=id), self.outer._Requirement.NO, stream=True, auto_status_code_handling = False) + stream = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.changeset["download"].format(id=id), stream=True, auto_status_code_handling = False) match stream.status_code: case 200: pass @@ -284,7 +282,6 @@ def upload(self, changeset_id: int, osmChange: OsmChange, make_osmChange_valid: response = self.outer._request( method=self.outer._RequestMethods.POST, url=self.outer._url.changeset["upload"].format(id=changeset_id), - auth_requirement=self.outer._Requirement.YES, body = osmChange.to_xml(changeset_id, make_osmChange_valid, work_on_copy), auto_status_code_handling=False ) diff --git a/src/osm_easy_api/api/endpoints/changeset_discussion.py b/src/osm_easy_api/api/endpoints/changeset_discussion.py index 21a97c2..21a8480 100644 --- a/src/osm_easy_api/api/endpoints/changeset_discussion.py +++ b/src/osm_easy_api/api/endpoints/changeset_discussion.py @@ -21,7 +21,7 @@ def comment(self, changeset_id: int, text: str) -> None: exceptions.ChangesetNotClosed: Changeset must be closed to add comment. exceptions.TooManyRequests: Request has been blocked due to rate limiting. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["comment"].format(id=changeset_id, text=urllib.parse.quote(text)), self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["comment"].format(id=changeset_id, text=urllib.parse.quote(text)), auto_status_code_handling=False) match response.status_code: case 200: pass @@ -38,7 +38,7 @@ def subscribe(self, changeset_id: int) -> None: Raises: exceptions.AlreadySubscribed: You are already subscribed to this changeset. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["subscribe"].format(id=changeset_id), self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["subscribe"].format(id=changeset_id), auto_status_code_handling=False) match response.status_code: case 200: pass @@ -54,7 +54,7 @@ def unsubscribe(self, changeset_id: int) -> None: Raises: exceptions.NotSubscribed: You are not subscribed to this changeset. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unsubscribe"].format(id=changeset_id), self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unsubscribe"].format(id=changeset_id), auto_status_code_handling=False) match response.status_code: case 200: pass @@ -71,7 +71,7 @@ def hide(self, comment_id: int) -> None: exceptions.NotAModerator: You are not a moderator. exceptions.IdNotFoundError: Comment with provided id not found. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id), self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id), auto_status_code_handling=False) match response.status_code: case 200: pass @@ -89,7 +89,7 @@ def unhide(self, comment_id: int) -> None: exceptions.NotAModerator: You are not a moderator. exceptions.IdNotFoundError: Comment with provided id not found. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id), self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id), auto_status_code_handling=False) match response.status_code: case 200: pass diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 4550f84..3549581 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -33,7 +33,7 @@ def create(self, element: Node | Way | Relation, changeset_id: int) -> int: """ element_name = element.__class__.__name__.lower() body = f"\n{element._to_xml(changeset_id).toprettyxml()}" - response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["create"].format(element_type=element_name), self.outer._Requirement.YES, body=body, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["create"].format(element_type=element_name), body=body, auto_status_code_handling=False) match response.status_code: case 200: pass @@ -62,7 +62,6 @@ def get(self, element: type[Node_Way_Relation], id: int) -> Node_Way_Relation : url = self.outer._url.elements["read"].format(element_type=element_name, id=id) status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -97,7 +96,7 @@ def update(self, element: Node | Way | Relation, changeset_id: int) -> int: element.changeset_id = changeset_id element_name = element.__class__.__name__.lower() body = f"\n{element._to_xml(element.changeset_id).toprettyxml()}" - response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["update"].format(element_type=element_name, id=element.id), self.outer._Requirement.YES, body=body, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["update"].format(element_type=element_name, id=element.id), body=body, auto_status_code_handling=False) match response.status_code: case 200: pass @@ -128,7 +127,7 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: element.changeset_id = changeset_id element_name = element.__class__.__name__.lower() body = f"\n{element._to_xml(element.changeset_id).toprettyxml()}" - response = self.outer._request(self.outer._RequestMethods.DELETE, self.outer._url.elements["delete"].format(element_type=element_name, id=element.id), self.outer._Requirement.YES, body=body, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.DELETE, self.outer._url.elements["delete"].format(element_type=element_name, id=element.id), body=body, auto_status_code_handling=False) match response.status_code: case 200: pass @@ -157,7 +156,6 @@ def history(self, element: type[Node_Way_Relation], id: int) -> list[Node_Way_Re url = self.outer._url.elements["history"].format(element_type=element_name, id=id) status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -191,7 +189,6 @@ def version(self, element: type[Node_Way_Relation], id: int, version: int) -> No url = self.outer._url.elements["version"].format(element_type=element_name, id=id, version=version) status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -227,7 +224,6 @@ def get_query(self, element: type[Node_Way_Relation], ids: list[int]) -> list[No url = self.outer._url.elements["multi_fetch"].format(element_type=element_name) + param status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -258,7 +254,6 @@ def relations(self, element: type[Node | Way | Relation], id: int) -> list[Relat url = self.outer._url.elements["relations"].format(element_type=element_name, id=id) generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=True) relations_list = [] @@ -280,7 +275,6 @@ def ways(self, node_id: int) -> list[Way]: url = self.outer._url.elements["ways"].format(id=node_id) generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=True) ways_list = [] @@ -308,7 +302,6 @@ def full(self, element: type[Way_Relation], id: int) -> Way_Relation: url = self.outer._url.elements["full"].format(element_type = element_name, id=id) status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -360,4 +353,4 @@ def redaction(self, element: type[Node | Way | Relation], id: int, version: int, redaction_id (int): https://www.openstreetmap.org/redactions """ element_name = element.__name__.lower() - self.outer._request(self.outer._RequestMethods.POST, self.outer._url.elements["redaction"].format(element_type=element_name, id=id, version=version, redaction_id=redaction_id), self.outer._Requirement.YES, auto_status_code_handling=True) \ No newline at end of file + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.elements["redaction"].format(element_type=element_name, id=id, version=version, redaction_id=redaction_id), auto_status_code_handling=True) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index b785a16..e31c531 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -20,6 +20,6 @@ def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_n top (int): Bounding box page_number (int): Which group of 5 000 points you want to get. """ - response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), self.outer._Requirement.NO, True, False) + response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), True, False) with open(file_to, "wb") as f_to: shutil.copyfileobj(response.raw, f_to) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index 9c0ee23..a90ba6b 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -82,7 +82,6 @@ def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) """ response = self.outer._request(method=self.outer._RequestMethods.GET, url=self.outer._url.misc["map"].format(left=left, bottom=bottom, right=right, top=top), - auth_requirement=self.outer._Requirement.OPTIONAL, stream=True, auto_status_code_handling=False ) diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index e64d93d..d15b9f2 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -80,7 +80,6 @@ def get(self, id: int) -> Note: """ status_code, generator = self.outer._get_generator( url=self.outer._url.note["get"].format(id=id), - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: @@ -112,7 +111,6 @@ def get_bbox(self, left: str, bottom: str, right: str, top: str, limit: int = 10 status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False ) @@ -136,7 +134,6 @@ def create(self, latitude: str, longitude: str, text: str) -> Note: """ generator = self.outer._post_generator( url=self.outer._url.note["create"].format(latitude=latitude, longitude=longitude, text=urllib.parse.quote(text)), - auth_requirement=self.outer._Requirement.OPTIONAL, auto_status_code_handling=True) return self._xml_to_notes_list(generator)[0] @@ -158,7 +155,6 @@ def comment(self, id: int, text: str) -> Note: """ status_code, generator = self.outer._post_generator( url=self.outer._url.note["comment"].format(id=id, text=urllib.parse.quote(text)), - auth_requirement=self.outer._Requirement.YES, auto_status_code_handling=False) match status_code: @@ -190,7 +186,6 @@ def close(self, id: int, text: str | None = None) -> Note: status_code, generator = self.outer._post_generator( url=url+param, - auth_requirement=self.outer._Requirement.YES, auto_status_code_handling=False) match status_code: @@ -222,7 +217,6 @@ def reopen(self, id: int, text: str | None = None) -> Note: status_code, generator = self.outer._post_generator( url=url+param, - auth_requirement=self.outer._Requirement.YES, auto_status_code_handling=False) match status_code: @@ -252,7 +246,6 @@ def hide(self, id: int, text: str | None = None) -> None: status_code, response = self.outer._request( method=self.outer._RequestMethods.DELETE, url=url+param, - auth_requirement=self.outer._Requirement.YES, stream=False, auto_status_code_handling=False ) @@ -292,7 +285,6 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int status_code, generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=False) match status_code: diff --git a/src/osm_easy_api/api/endpoints/user.py b/src/osm_easy_api/api/endpoints/user.py index 69eeeaf..74e8fba 100644 --- a/src/osm_easy_api/api/endpoints/user.py +++ b/src/osm_easy_api/api/endpoints/user.py @@ -65,7 +65,6 @@ def get(self, id: int) -> User: """ generator = self.outer._get_generator( url=self.outer._url.user["get"].format(id=id), - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=True) return self._xml_to_users_list(generator)[0] @@ -85,7 +84,6 @@ def get_query(self, ids: list[int]) -> list[User]: param = param[:-1] generator = self.outer._get_generator( url=self.outer._url.user["get_query"] + param, - auth_requirement=self.outer._Requirement.NO, auto_status_code_handling=True) return self._xml_to_users_list(generator) @@ -98,7 +96,6 @@ def get_current(self) -> User: """ generator = self.outer._get_generator( url=self.outer._url.user["get_current"], - auth_requirement=self.outer._Requirement.YES, auto_status_code_handling=True) return self._xml_to_users_list(generator)[0] @@ -118,7 +115,7 @@ def get_preferences(self, key: str | None = None) -> dict[str, str]: url = self.outer._url.user["preferences"] if key: url += f"/{key}" - response = self.outer._request(self.outer._RequestMethods.GET, url, self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.GET, url, auto_status_code_handling=False) match response.status_code: case 200: pass case 404: raise ValueError("Preference not found") @@ -127,7 +124,6 @@ def get_preferences(self, key: str | None = None) -> dict[str, str]: generator = self.outer._get_generator( url=url, - auth_requirement=self.outer._Requirement.YES, auto_status_code_handling=True) preferences = {} @@ -153,7 +149,7 @@ def set_preferences(self, preferences: dict[str, str]) -> None: root.appendChild(preferences_element) xml_str = root.toprettyxml(indent="\t") - self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.user["preferences"], self.outer._Requirement.YES, stream=True, body=xml_str) + self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.user["preferences"], stream=True, body=xml_str) def delete_preference(self, key: str) -> None: """Deletes only one preference with given key. @@ -166,7 +162,7 @@ def delete_preference(self, key: str) -> None: """ url = self.outer._url.user["preferences"] url += f"/{key}" - response = self.outer._request(self.outer._RequestMethods.DELETE, url, self.outer._Requirement.YES, auto_status_code_handling=False) + response = self.outer._request(self.outer._RequestMethods.DELETE, url, auto_status_code_handling=False) match response.status_code: case 200: pass case 404: raise ValueError("Preference not found") diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 229ad10..d0d11b9 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -6,13 +6,18 @@ class TestApi(unittest.TestCase): def test_initialize(self): Api("https://test.pl") - def test_credintials(self): - api = Api(username="abc", password="cba") - self.assertIsNotNone(api._auth) - self.assertEqual(api._auth.username.decode(), "abc") - self.assertEqual(api._auth.password.decode(), "cba") + def test_empty_headers(self): + api = Api() + self.assertEqual(api._headers, {}) - api = Api(username="ęśąćź", password="ąęźż") - self.assertIsNotNone(api._auth) - self.assertEqual(api._auth.username.decode(), "ęśąćź") - self.assertEqual(api._auth.password.decode(), "ąęźż") \ No newline at end of file + def test_authorization_header(self): + api = Api(access_token="TOKEN") + self.assertEqual(api._headers, {"Authorization": "Bearer TOKEN"}) + + def test_user_agent_header(self): + api = Api(user_agent="AGENT") + self.assertEqual(api._headers, {"User-Agent": "AGENT"}) + + def test_authorization_and_user_agent_header(self): + api = Api(access_token="TOKEN", user_agent="AGENT") + self.assertEqual(api._headers, {"Authorization": "Bearer TOKEN", "User-Agent": "AGENT"}) \ No newline at end of file From 2820958bfbf7dbc147c9e515f576947905f8c223 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:53:01 +0100 Subject: [PATCH 02/50] New status code handler --- CHANGELOG.md | 3 + src/osm_easy_api/api/api.py | 31 ++--- src/osm_easy_api/api/endpoints/changeset.py | 58 +++------ .../api/endpoints/changeset_discussion.py | 38 +----- src/osm_easy_api/api/endpoints/elements.py | 110 +++++------------- src/osm_easy_api/api/endpoints/gpx.py | 2 +- src/osm_easy_api/api/endpoints/misc.py | 11 +- src/osm_easy_api/api/endpoints/notes.py | 78 +++---------- src/osm_easy_api/api/endpoints/user.py | 25 +--- src/osm_easy_api/api/exceptions.py | 11 +- tests/api/test_api.py | 2 + 11 files changed, 110 insertions(+), 259 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1436e13..f836122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for `oAuth2`: `access_token` parameter in `Api` class constructor. +### Changed +- Changed the way http errors are handled. + ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index 40dbcc4..def6bfc 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -9,6 +9,7 @@ from ._URLs import URLs from .endpoints import Misc_Container, Changeset_Container, Elements_Container, Gpx_Container, User_Container, Notes_Container +from .exceptions import STATUS_CODE_EXCEPTIONS class Api(): """Class used to communicate with API.""" @@ -37,10 +38,16 @@ def __init__(self, url: str = "https://master.apis.dev.openstreetmap.org", acces if user_agent: self._headers.update({"User-Agent": user_agent}) - def _request(self, method: _RequestMethods, url: str, stream: bool = False, auto_status_code_handling: bool = True, body = None) -> "Response": + def _request(self, method: _RequestMethods, url: str, stream: bool = False, custom_status_code_exceptions: dict = {int: Exception}, body = None) -> "Response": response = requests.request(str(method), url, stream=stream, data=body.encode('utf-8') if body else None, headers=self._headers) - if auto_status_code_handling: assert response.status_code == 200, f"Invalid (and unexpected) response code {response.status_code} for {url}" - return response + if response.status_code == 200: return response + + exception = custom_status_code_exceptions.get(response.status_code, None) or STATUS_CODE_EXCEPTIONS.get(response.status_code, None) + if not exception: exception = STATUS_CODE_EXCEPTIONS.get(response.status_code, None) + if not exception: exception = custom_status_code_exceptions.get(-1, None) + if not exception: raise NotImplementedError(f"Invalid (and unexpected) response code {response.status_code} for {url}. Please report it on GitHub.") + if str(exception): raise type(exception)(str(exception).format(TEXT=response.text, CODE=response.status_code)) from exception + raise exception @staticmethod def _raw_stream_parser(xml_raw_stream: "HTTPResponse") -> Generator[ElementTree.Element, None, None]: @@ -48,18 +55,12 @@ def _raw_stream_parser(xml_raw_stream: "HTTPResponse") -> Generator[ElementTree. for event, element in iterator: yield element - def _get_generator(self, url: str, auto_status_code_handling: bool = True) -> Generator[ElementTree.Element, None, None] | Tuple[int, Generator[ElementTree.Element, None, None]]: - response = self._request(self._RequestMethods.GET, url, auto_status_code_handling=auto_status_code_handling, stream=True) + def _get_generator(self, url: str, custom_status_code_exceptions: dict = {int: Exception}) -> Generator[ElementTree.Element, None, None]: + response = self._request(self._RequestMethods.GET, url, custom_status_code_exceptions=custom_status_code_exceptions, stream=True) response.raw.decode_content = True - if auto_status_code_handling: - return self._raw_stream_parser(response.raw) - else: - return (response.status_code, self._raw_stream_parser(response.raw)) + return self._raw_stream_parser(response.raw) - def _post_generator(self, url: str, auto_status_code_handling: bool = True) -> Generator[ElementTree.Element, None, None] | Tuple[int, Generator[ElementTree.Element, None, None]]: - response = self._request(self._RequestMethods.POST, url, auto_status_code_handling=auto_status_code_handling, stream=True) + def _post_generator(self, url: str, custom_status_code_exceptions: dict = {int: Exception}) -> Generator[ElementTree.Element, None, None]: + response = self._request(self._RequestMethods.POST, url, custom_status_code_exceptions=custom_status_code_exceptions, stream=True) response.raw.decode_content = True - if auto_status_code_handling: - return self._raw_stream_parser(response.raw) - else: - return (response.status_code, self._raw_stream_parser(response.raw)) \ No newline at end of file + return self._raw_stream_parser(response.raw) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 54f6d9b..b96948a 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -101,14 +101,7 @@ def get(self, id: int, include_discussion: bool = False) -> Changeset: """ include_discussion_text = "true" if include_discussion else "false" param = f"{id}?include_discussion={include_discussion_text}" - status_code, generator = self.outer._get_generator( - url=join_url(self.outer._url.changeset["get"], param), - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + generator = self.outer._get_generator(url=join_url(self.outer._url.changeset["get"], param)) return self._xml_to_changesets_list(generator, include_discussion)[0] # type: ignore @@ -158,15 +151,9 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ param += "&" param+=f"limit={limit}" - status_code, generator = self.outer._get_generator( + generator = self.outer._get_generator( url=join_url(self.outer._url.changeset["get_query"], param), - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 400: raise ValueError("Invalid arguments. See https://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_/api/0.6/changesets for more info.") - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={400: ValueError("Invalid arguments. See https://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_/api/0.6/changesets for more info.")}) return self._xml_to_changesets_list(generator) # type: ignore @@ -208,13 +195,7 @@ def update(self, id: int, comment: str | None = None, tags: Tags | None = None) xml_str = root.toprettyxml(indent="\t") response = self.outer._request(self.outer._RequestMethods.PUT, - self.outer._url.changeset["update"].format(id=id), body=xml_str, stream=True, auto_status_code_handling = False) - - match response.status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 409: raise exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor(response.text) - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + self.outer._url.changeset["update"].format(id=id), body=xml_str, stream=True, custom_status_code_exceptions={409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor("{TEXT}")}) response.raw.decode_content = True return self._xml_to_changesets_list(self.outer._raw_stream_parser(response.raw), True)[0] @@ -229,12 +210,7 @@ def close(self, id: int) -> None: exceptions.IdNotFoundError: There is no changeset with given ID. exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: The changeset was already closer or you are not the author. """ - response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.changeset["close"].format(id = id), auto_status_code_handling = False) - match response.status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 409: raise exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor(response.text) - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.changeset["close"].format(id = id), custom_status_code_exceptions={409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor("{TEXT}")}) def download(self, id: int) -> Generator[Tuple['Action', 'Node | Way | Relation'], None, None]: """Download changes made in changeset. Like in 'diff' module. @@ -248,13 +224,8 @@ def download(self, id: int) -> Generator[Tuple['Action', 'Node | Way | Relation' Yields: Generator: Diff generator like in 'diff' module. """ - stream = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.changeset["download"].format(id=id), stream=True, auto_status_code_handling = False) + stream = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.changeset["download"].format(id=id), stream=True) - match stream.status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {stream.status_code}. Please report it on github." # pragma: no cover - stream.raw.decode_content = True def generator() -> Generator[tuple['Action', 'Node | Way | Relation'], None, None]: gen = OsmChange_parser_generator(stream.raw, None) @@ -279,15 +250,14 @@ def upload(self, changeset_id: int, osmChange: OsmChange, make_osmChange_valid: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: Changeset already closed or you are not an author. ValueError: Unexpected but correct error. """ - response = self.outer._request( + self.outer._request( method=self.outer._RequestMethods.POST, url=self.outer._url.changeset["upload"].format(id=changeset_id), body = osmChange.to_xml(changeset_id, make_osmChange_valid, work_on_copy), - auto_status_code_handling=False - ) - match response.status_code: - case 200: pass - case 400: raise exceptions.ErrorWhenParsingXML(response.text) - case 404: raise exceptions.IdNotFoundError(response.text) - case 409: raise exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor() - case _: raise ValueError("Unexpected but correct error. Status code:", response.status_code) \ No newline at end of file + custom_status_code_exceptions= { + 400: exceptions.ErrorWhenParsingXML("{TEXT}"), + 404: exceptions.IdNotFoundError("{TEXT}"), + 409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor(), + -1: ValueError("Unexpected but correct error. Status code: {CODE}") + } + ) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/changeset_discussion.py b/src/osm_easy_api/api/endpoints/changeset_discussion.py index 21a8480..e70e9e7 100644 --- a/src/osm_easy_api/api/endpoints/changeset_discussion.py +++ b/src/osm_easy_api/api/endpoints/changeset_discussion.py @@ -21,13 +21,7 @@ def comment(self, changeset_id: int, text: str) -> None: exceptions.ChangesetNotClosed: Changeset must be closed to add comment. exceptions.TooManyRequests: Request has been blocked due to rate limiting. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["comment"].format(id=changeset_id, text=urllib.parse.quote(text)), auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 409: raise exceptions.ChangesetNotClosed() - case 429: raise exceptions.TooManyRequests() - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["comment"].format(id=changeset_id, text=urllib.parse.quote(text)), custom_status_code_exceptions={409: exceptions.ChangesetNotClosed()}) def subscribe(self, changeset_id: int) -> None: """Subscribe to the discussion to receive notifications for new comments. @@ -38,12 +32,7 @@ def subscribe(self, changeset_id: int) -> None: Raises: exceptions.AlreadySubscribed: You are already subscribed to this changeset. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["subscribe"].format(id=changeset_id), auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 409: raise exceptions.AlreadySubscribed() - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["subscribe"].format(id=changeset_id), custom_status_code_exceptions={409: exceptions.AlreadySubscribed()}) def unsubscribe(self, changeset_id: int) -> None: """Unsubscribe from discussion to stop receiving notifications. @@ -54,12 +43,7 @@ def unsubscribe(self, changeset_id: int) -> None: Raises: exceptions.NotSubscribed: You are not subscribed to this changeset. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unsubscribe"].format(id=changeset_id), auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 404: raise exceptions.NotSubscribed() - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unsubscribe"].format(id=changeset_id), custom_status_code_exceptions={404: exceptions.NotSubscribed()}) def hide(self, comment_id: int) -> None: """Set visible flag on changeset comment to false. MODERATOR ONLY! @@ -71,13 +55,7 @@ def hide(self, comment_id: int) -> None: exceptions.NotAModerator: You are not a moderator. exceptions.IdNotFoundError: Comment with provided id not found. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id), auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 403: raise exceptions.NotAModerator() - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id), custom_status_code_exceptions={403: exceptions.NotAModerator()}) def unhide(self, comment_id: int) -> None: """Set visible flag on changeset comment to true. MODERATOR ONLY! @@ -89,10 +67,4 @@ def unhide(self, comment_id: int) -> None: exceptions.NotAModerator: You are not a moderator. exceptions.IdNotFoundError: Comment with provided id not found. """ - response = self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id), auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 403: raise exceptions.NotAModerator() - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover \ No newline at end of file + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id), custom_status_code_exceptions={403: exceptions.NotAModerator()}) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 3549581..1e8fe7f 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -33,14 +33,10 @@ def create(self, element: Node | Way | Relation, changeset_id: int) -> int: """ element_name = element.__class__.__name__.lower() body = f"\n{element._to_xml(changeset_id).toprettyxml()}" - response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["create"].format(element_type=element_name), body=body, auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 400: raise ValueError(response.content) - case 409: raise exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor(response.content) - case 412: raise ValueError(response.content) - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover + response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["create"].format(element_type=element_name), body=body, + custom_status_code_exceptions={ + 409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor("{TEXT}") + }) return int(response.text) @@ -60,15 +56,9 @@ def get(self, element: type[Node_Way_Relation], id: int) -> Node_Way_Relation : """"" element_name = element.__name__.lower() url = self.outer._url.elements["read"].format(element_type=element_name, id=id) - status_code, generator = self.outer._get_generator( + generator = self.outer._get_generator( url=url, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={410: exceptions.ElementDeleted()}) for elem in generator: if elem.tag in ("node", "way", "relation"): @@ -96,16 +86,12 @@ def update(self, element: Node | Way | Relation, changeset_id: int) -> int: element.changeset_id = changeset_id element_name = element.__class__.__name__.lower() body = f"\n{element._to_xml(element.changeset_id).toprettyxml()}" - response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["update"].format(element_type=element_name, id=element.id), body=body, auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 400: raise ValueError(response.content) - case 409: raise ValueError(response.content) - case 404: raise exceptions.IdNotFoundError() - case 412: raise exceptions.IdNotFoundError(response.content) - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover - return int(response.content) + response = self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.elements["update"].format(element_type=element_name, id=element.id), body=body, custom_status_code_exceptions={ + 409: ValueError("{TEXT}"), + 412: exceptions.IdNotFoundError("{TEXT}"), + }) + + return int(response.content) # FIXME: Should be text? def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: """Deletes element. @@ -127,17 +113,11 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: element.changeset_id = changeset_id element_name = element.__class__.__name__.lower() body = f"\n{element._to_xml(element.changeset_id).toprettyxml()}" - response = self.outer._request(self.outer._RequestMethods.DELETE, self.outer._url.elements["delete"].format(element_type=element_name, id=element.id), body=body, auto_status_code_handling=False) - - match response.status_code: - case 200: pass - case 400: raise ValueError(response.content) - case 404: raise exceptions.IdNotFoundError() - case 409: raise exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor(response.content) - case 410: raise exceptions.ElementDeleted() - case 412: raise ValueError(response.content) - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover - return int(response.content) + response = self.outer._request(self.outer._RequestMethods.DELETE, self.outer._url.elements["delete"].format(element_type=element_name, id=element.id), body=body, custom_status_code_exceptions={ + 409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor("{TEXT}") + }) + + return int(response.content) # FIXME: Should be text? def history(self, element: type[Node_Way_Relation], id: int) -> list[Node_Way_Relation]: """Returns all old versions of element. @@ -154,14 +134,8 @@ def history(self, element: type[Node_Way_Relation], id: int) -> list[Node_Way_Re """ element_name = element.__name__.lower() url = self.outer._url.elements["history"].format(element_type=element_name, id=id) - status_code, generator = self.outer._get_generator( - url=url, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + generator = self.outer._get_generator( + url=url) objects_list = [] for elem in generator: @@ -187,16 +161,12 @@ def version(self, element: type[Node_Way_Relation], id: int, version: int) -> No """ element_name = element.__name__.lower() url = self.outer._url.elements["version"].format(element_type=element_name, id=id, version=version) - status_code, generator = self.outer._get_generator( + generator = self.outer._get_generator( url=url, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 403: raise exceptions.IdNotFoundError("This version of the element is not available (due to redaction)") - case 404: raise exceptions.IdNotFoundError() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover - + custom_status_code_exceptions={ + 403: exceptions.IdNotFoundError("This version of the element is not available (due to redaction)") + }) + for elem in generator: if elem.tag in ("node", "way", "relation"): return _element_to_osm_object(elem) @@ -222,16 +192,11 @@ def get_query(self, element: type[Node_Way_Relation], ids: list[int]) -> list[No for id in ids: param += f"{id}," param = param[:-1] url = self.outer._url.elements["multi_fetch"].format(element_type=element_name) + param - status_code, generator = self.outer._get_generator( + generator = self.outer._get_generator( url=url, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 400: raise ValueError() - case 404: raise exceptions.IdNotFoundError() - case 414: raise ValueError("URL too long (too many ids)") - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={ + 414: ValueError("URL too long (too many ids)") + }) objects_list = [] for elem in generator: @@ -253,8 +218,7 @@ def relations(self, element: type[Node | Way | Relation], id: int) -> list[Relat element_name = element.__name__.lower() url = self.outer._url.elements["relations"].format(element_type=element_name, id=id) generator = self.outer._get_generator( - url=url, - auto_status_code_handling=True) + url=url) relations_list = [] for elem in generator: @@ -273,9 +237,7 @@ def ways(self, node_id: int) -> list[Way]: list[Way]: List of ways. """ url = self.outer._url.elements["ways"].format(id=node_id) - generator = self.outer._get_generator( - url=url, - auto_status_code_handling=True) + generator = self.outer._get_generator(url=url) ways_list = [] for elem in generator: @@ -300,15 +262,7 @@ def full(self, element: type[Way_Relation], id: int) -> Way_Relation: """ element_name = element.__name__.lower() url = self.outer._url.elements["full"].format(element_type = element_name, id=id) - status_code, generator = self.outer._get_generator( - url=url, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + generator = self.outer._get_generator(url=url) nodes_dict: dict[int, Node] = {} ways_dict: dict[int, Way] = {} @@ -353,4 +307,4 @@ def redaction(self, element: type[Node | Way | Relation], id: int, version: int, redaction_id (int): https://www.openstreetmap.org/redactions """ element_name = element.__name__.lower() - self.outer._request(self.outer._RequestMethods.POST, self.outer._url.elements["redaction"].format(element_type=element_name, id=id, version=version, redaction_id=redaction_id), auto_status_code_handling=True) \ No newline at end of file + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.elements["redaction"].format(element_type=element_name, id=id, version=version, redaction_id=redaction_id)) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index e31c531..3cc2663 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -20,6 +20,6 @@ def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_n top (int): Bounding box page_number (int): Which group of 5 000 points you want to get. """ - response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), True, False) + response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), stream=True) with open(file_to, "wb") as f_to: shutil.copyfileobj(response.raw, f_to) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index a90ba6b..613f0f0 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -83,15 +83,12 @@ def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) response = self.outer._request(method=self.outer._RequestMethods.GET, url=self.outer._url.misc["map"].format(left=left, bottom=bottom, right=right, top=top), stream=True, - auto_status_code_handling=False + custom_status_code_exceptions={ + 400: exceptions.LimitsExceeded("You are trying to download too much data."), + 509: exceptions.LimitsExceeded("You have downloaded too much data. Please try again later. See https://wiki.openstreetmap.org/wiki/Developer_FAQ#I've_been_blocked_from_the_API_for_downloading_too_much._Now_what?") + } ) - match response.status_code: - case 200: pass - case 400: raise exceptions.LimitsExceeded("You are trying to download too much data.") - case 509: raise exceptions.LimitsExceeded("You have downloaded too much data. Please try again later. See https://wiki.openstreetmap.org/wiki/Developer_FAQ#I've_been_blocked_from_the_API_for_downloading_too_much._Now_what?") - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." # pragma: no cover - response.raw.decode_content = True def generator(): gen = OsmChange_parser_generator(response.raw, None) diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index d15b9f2..6e4b508 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -78,15 +78,8 @@ def get(self, id: int) -> Note: Returns: Note: Note object. """ - status_code, generator = self.outer._get_generator( - url=self.outer._url.note["get"].format(id=id), - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + generator = self.outer._get_generator( + url=self.outer._url.note["get"].format(id=id)) return self._xml_to_notes_list(generator)[0] @@ -109,16 +102,11 @@ def get_bbox(self, left: str, bottom: str, right: str, top: str, limit: int = 10 """ url=self.outer._url.note["get_bbox"].format(left=left, bottom=bottom, right=right, top=top, limit=limit, closed_days=closed_days) - status_code, generator = self.outer._get_generator( + generator = self.outer._get_generator( url=url, - auto_status_code_handling=False + custom_status_code_exceptions={400: ValueError("Limits exceeded")} ) - match status_code: - case 200: pass - case 400: raise ValueError("Limits exceeded") - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover - return self._xml_to_notes_list(generator) def create(self, latitude: str, longitude: str, text: str) -> Note: @@ -133,8 +121,7 @@ def create(self, latitude: str, longitude: str, text: str) -> Note: Note: Object of newly created note. """ generator = self.outer._post_generator( - url=self.outer._url.note["create"].format(latitude=latitude, longitude=longitude, text=urllib.parse.quote(text)), - auto_status_code_handling=True) + url=self.outer._url.note["create"].format(latitude=latitude, longitude=longitude, text=urllib.parse.quote(text))) return self._xml_to_notes_list(generator)[0] @@ -153,16 +140,9 @@ def comment(self, id: int, text: str) -> Note: Returns: Note: Note object of commented note """ - status_code, generator = self.outer._post_generator( + generator = self.outer._post_generator( url=self.outer._url.note["comment"].format(id=id, text=urllib.parse.quote(text)), - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 409: raise exceptions.NoteAlreadyClosed() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={409: exceptions.NoteAlreadyClosed()}) return self._xml_to_notes_list(generator)[0] @@ -184,16 +164,9 @@ def close(self, id: int, text: str | None = None) -> Note: url = self.outer._url.note["close"].format(id=id, text=text) param = f"?text={text}" if text else "" - status_code, generator = self.outer._post_generator( + generator = self.outer._post_generator( url=url+param, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 409: raise exceptions.NoteAlreadyClosed() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={409: exceptions.NoteAlreadyClosed()}) return self._xml_to_notes_list(generator)[0] @@ -215,16 +188,9 @@ def reopen(self, id: int, text: str | None = None) -> Note: url = self.outer._url.note["reopen"].format(id=id, text=text) param = f"?text={text}" if text else "" - status_code, generator = self.outer._post_generator( + generator = self.outer._post_generator( url=url+param, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 404: raise exceptions.IdNotFoundError() - case 409: raise exceptions.NoteAlreadyOpen() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={409: exceptions.NoteAlreadyOpen()}) return self._xml_to_notes_list(generator)[0] @@ -243,19 +209,14 @@ def hide(self, id: int, text: str | None = None) -> None: url = self.outer._url.note["hide"].format(id=id, text=text) param = f"?text={text}" if text else "" - status_code, response = self.outer._request( + self.outer._request( method=self.outer._RequestMethods.DELETE, url=url+param, stream=False, - auto_status_code_handling=False + custom_status_code_exceptions={ + 403: exceptions.NotAModerator() + } ) - - match status_code: - case 200: pass - case 403: raise exceptions.NotAModerator() - case 404: raise exceptions.IdNotFoundError() - case 410: raise exceptions.ElementDeleted() - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int | None = None, from_date: str | None = None, to_date: str | None = None, sort: str = "updated_at", order: str = "newest") -> list[Note]: """Search for notes with initial text and comments. @@ -283,14 +244,9 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int if sort: url += f"&sort={sort}" if order: url += f"&order={order}" - status_code, generator = self.outer._get_generator( + generator = self.outer._get_generator( url=url, - auto_status_code_handling=False) - - match status_code: - case 200: pass - case 400: raise ValueError("Limits exceeded") - case _: assert False, f"Unexpected response status code {status_code}. Please report it on github." # pragma: no cover + custom_status_code_exceptions={400: ValueError("Limits exceeded")}) try: return self._xml_to_notes_list(generator) diff --git a/src/osm_easy_api/api/endpoints/user.py b/src/osm_easy_api/api/endpoints/user.py index 74e8fba..5514120 100644 --- a/src/osm_easy_api/api/endpoints/user.py +++ b/src/osm_easy_api/api/endpoints/user.py @@ -64,8 +64,7 @@ def get(self, id: int) -> User: User: User object. """ generator = self.outer._get_generator( - url=self.outer._url.user["get"].format(id=id), - auto_status_code_handling=True) + url=self.outer._url.user["get"].format(id=id)) return self._xml_to_users_list(generator)[0] @@ -83,8 +82,7 @@ def get_query(self, ids: list[int]) -> list[User]: param += f"{id}," param = param[:-1] generator = self.outer._get_generator( - url=self.outer._url.user["get_query"] + param, - auto_status_code_handling=True) + url=self.outer._url.user["get_query"] + param) return self._xml_to_users_list(generator) @@ -95,8 +93,7 @@ def get_current(self) -> User: User: User object. """ generator = self.outer._get_generator( - url=self.outer._url.user["get_current"], - auto_status_code_handling=True) + url=self.outer._url.user["get_current"]) return self._xml_to_users_list(generator)[0] @@ -115,16 +112,10 @@ def get_preferences(self, key: str | None = None) -> dict[str, str]: url = self.outer._url.user["preferences"] if key: url += f"/{key}" - response = self.outer._request(self.outer._RequestMethods.GET, url, auto_status_code_handling=False) - match response.status_code: - case 200: pass - case 404: raise ValueError("Preference not found") - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." + response = self.outer._request(self.outer._RequestMethods.GET, url, custom_status_code_exceptions={404: ValueError("Preference not found")}) return {key: response.text} - generator = self.outer._get_generator( - url=url, - auto_status_code_handling=True) + generator = self.outer._get_generator(url=url) preferences = {} for element in generator: @@ -162,8 +153,4 @@ def delete_preference(self, key: str) -> None: """ url = self.outer._url.user["preferences"] url += f"/{key}" - response = self.outer._request(self.outer._RequestMethods.DELETE, url, auto_status_code_handling=False) - match response.status_code: - case 200: pass - case 404: raise ValueError("Preference not found") - case _: assert False, f"Unexpected response status code {response.status_code}. Please report it on github." \ No newline at end of file + self.outer._request(self.outer._RequestMethods.DELETE, url, custom_status_code_exceptions={404: ValueError("Preference not found")}) \ No newline at end of file diff --git a/src/osm_easy_api/api/exceptions.py b/src/osm_easy_api/api/exceptions.py index 9867469..7a6758f 100644 --- a/src/osm_easy_api/api/exceptions.py +++ b/src/osm_easy_api/api/exceptions.py @@ -41,4 +41,13 @@ class NoteAlreadyOpen(Exception): pass class TooManyRequests(Exception): - pass \ No newline at end of file + pass + + +STATUS_CODE_EXCEPTIONS = { + 400: ValueError("{TEXT}"), + 404: IdNotFoundError(), + 410: ElementDeleted(), + 412: ValueError("{TEXT}"), + 429: TooManyRequests(), +} diff --git a/tests/api/test_api.py b/tests/api/test_api.py index d0d11b9..e28175a 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,3 +1,5 @@ +# TODO: Do not use LOGIN and PASSWORD in tests. + import unittest from osm_easy_api import Api From 702330372d3dae9bac1eee678442dc1cf50bdf69 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:11:20 +0100 Subject: [PATCH 03/50] Fixed duplicated code --- CHANGELOG.md | 2 +- src/osm_easy_api/api/api.py | 11 +++-------- src/osm_easy_api/api/endpoints/changeset.py | 5 +++-- src/osm_easy_api/api/endpoints/elements.py | 18 +++++++++--------- src/osm_easy_api/api/endpoints/misc.py | 6 +++--- src/osm_easy_api/api/endpoints/notes.py | 21 ++++++++++++++------- src/osm_easy_api/api/endpoints/user.py | 11 +++++++---- 7 files changed, 40 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f836122..ab1f0df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for `oAuth2`: `access_token` parameter in `Api` class constructor. ### Changed -- Changed the way http errors are handled. +- The way http errors are handled. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index def6bfc..3dadf35 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -54,13 +54,8 @@ def _raw_stream_parser(xml_raw_stream: "HTTPResponse") -> Generator[ElementTree. iterator = ElementTree.iterparse(xml_raw_stream, events=('end', )) for event, element in iterator: yield element - - def _get_generator(self, url: str, custom_status_code_exceptions: dict = {int: Exception}) -> Generator[ElementTree.Element, None, None]: - response = self._request(self._RequestMethods.GET, url, custom_status_code_exceptions=custom_status_code_exceptions, stream=True) - response.raw.decode_content = True - return self._raw_stream_parser(response.raw) - - def _post_generator(self, url: str, custom_status_code_exceptions: dict = {int: Exception}) -> Generator[ElementTree.Element, None, None]: - response = self._request(self._RequestMethods.POST, url, custom_status_code_exceptions=custom_status_code_exceptions, stream=True) + + def _request_generator(self, method: _RequestMethods, url: str, custom_status_code_exceptions: dict = {int: Exception}) -> Generator[ElementTree.Element, None, None]: + response = self._request(method=method, url=url, stream=True, custom_status_code_exceptions=custom_status_code_exceptions) response.raw.decode_content = True return self._raw_stream_parser(response.raw) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index b96948a..a89eb80 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -101,7 +101,7 @@ def get(self, id: int, include_discussion: bool = False) -> Changeset: """ include_discussion_text = "true" if include_discussion else "false" param = f"{id}?include_discussion={include_discussion_text}" - generator = self.outer._get_generator(url=join_url(self.outer._url.changeset["get"], param)) + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=join_url(self.outer._url.changeset["get"], param)) return self._xml_to_changesets_list(generator, include_discussion)[0] # type: ignore @@ -151,7 +151,8 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ param += "&" param+=f"limit={limit}" - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=join_url(self.outer._url.changeset["get_query"], param), custom_status_code_exceptions={400: ValueError("Invalid arguments. See https://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_/api/0.6/changesets for more info.")}) diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 1e8fe7f..6edf6a3 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -56,7 +56,7 @@ def get(self, element: type[Node_Way_Relation], id: int) -> Node_Way_Relation : """"" element_name = element.__name__.lower() url = self.outer._url.elements["read"].format(element_type=element_name, id=id) - generator = self.outer._get_generator( + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url, custom_status_code_exceptions={410: exceptions.ElementDeleted()}) @@ -134,8 +134,7 @@ def history(self, element: type[Node_Way_Relation], id: int) -> list[Node_Way_Re """ element_name = element.__name__.lower() url = self.outer._url.elements["history"].format(element_type=element_name, id=id) - generator = self.outer._get_generator( - url=url) + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) objects_list = [] for elem in generator: @@ -161,7 +160,8 @@ def version(self, element: type[Node_Way_Relation], id: int, version: int) -> No """ element_name = element.__name__.lower() url = self.outer._url.elements["version"].format(element_type=element_name, id=id, version=version) - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=url, custom_status_code_exceptions={ 403: exceptions.IdNotFoundError("This version of the element is not available (due to redaction)") @@ -192,7 +192,8 @@ def get_query(self, element: type[Node_Way_Relation], ids: list[int]) -> list[No for id in ids: param += f"{id}," param = param[:-1] url = self.outer._url.elements["multi_fetch"].format(element_type=element_name) + param - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=url, custom_status_code_exceptions={ 414: ValueError("URL too long (too many ids)") @@ -217,8 +218,7 @@ def relations(self, element: type[Node | Way | Relation], id: int) -> list[Relat """ element_name = element.__name__.lower() url = self.outer._url.elements["relations"].format(element_type=element_name, id=id) - generator = self.outer._get_generator( - url=url) + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) relations_list = [] for elem in generator: @@ -237,7 +237,7 @@ def ways(self, node_id: int) -> list[Way]: list[Way]: List of ways. """ url = self.outer._url.elements["ways"].format(id=node_id) - generator = self.outer._get_generator(url=url) + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) ways_list = [] for elem in generator: @@ -262,7 +262,7 @@ def full(self, element: type[Way_Relation], id: int) -> Way_Relation: """ element_name = element.__name__.lower() url = self.outer._url.elements["full"].format(element_type = element_name, id=id) - generator = self.outer._get_generator(url=url) + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) nodes_dict: dict[int, Node] = {} ways_dict: dict[int, Way] = {} diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index 613f0f0..c7f0a93 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -21,7 +21,7 @@ def versions(self) -> list: Returns: list: List of supported versions by instance. """ - gen: Generator['ElementTree.Element', None, None] = self.outer._get_generator(self.outer._url.misc["versions"]) + gen: Generator['ElementTree.Element', None, None] = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=self.outer._url.misc["versions"]) versions = [] for element in gen: if element.tag == "version": versions.append(element.text) @@ -55,7 +55,7 @@ def policy_parser(dict: dict, policy_element: "ElementTree.Element") -> None: dict["policy"]["imagery"]["blacklist_regex"].append(blacklist.attrib["regex"]) HEAD_TAGS = ("osm", "api", "policy") - gen: Generator['ElementTree.Element', None, None] = self.outer._get_generator(self.outer._url.misc["capabilities"]) + gen: Generator['ElementTree.Element', None, None] = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=self.outer._url.misc["capabilities"]) return_dict = {} for element in gen: @@ -103,7 +103,7 @@ def permissions(self) -> list: Returns: list: List of permissions names. """ - gen = self.outer._get_generator(self.outer._url.misc["permissions"]) + gen = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=self.outer._url.misc["permissions"]) return_permission_list = [] for element in gen: diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index 6e4b508..ded1f52 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -78,7 +78,8 @@ def get(self, id: int) -> Note: Returns: Note: Note object. """ - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=self.outer._url.note["get"].format(id=id)) return self._xml_to_notes_list(generator)[0] @@ -102,7 +103,8 @@ def get_bbox(self, left: str, bottom: str, right: str, top: str, limit: int = 10 """ url=self.outer._url.note["get_bbox"].format(left=left, bottom=bottom, right=right, top=top, limit=limit, closed_days=closed_days) - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=url, custom_status_code_exceptions={400: ValueError("Limits exceeded")} ) @@ -120,7 +122,8 @@ def create(self, latitude: str, longitude: str, text: str) -> Note: Returns: Note: Object of newly created note. """ - generator = self.outer._post_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.POST, url=self.outer._url.note["create"].format(latitude=latitude, longitude=longitude, text=urllib.parse.quote(text))) return self._xml_to_notes_list(generator)[0] @@ -140,7 +143,8 @@ def comment(self, id: int, text: str) -> Note: Returns: Note: Note object of commented note """ - generator = self.outer._post_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.POST, url=self.outer._url.note["comment"].format(id=id, text=urllib.parse.quote(text)), custom_status_code_exceptions={409: exceptions.NoteAlreadyClosed()}) @@ -164,7 +168,8 @@ def close(self, id: int, text: str | None = None) -> Note: url = self.outer._url.note["close"].format(id=id, text=text) param = f"?text={text}" if text else "" - generator = self.outer._post_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.POST, url=url+param, custom_status_code_exceptions={409: exceptions.NoteAlreadyClosed()}) @@ -188,7 +193,8 @@ def reopen(self, id: int, text: str | None = None) -> Note: url = self.outer._url.note["reopen"].format(id=id, text=text) param = f"?text={text}" if text else "" - generator = self.outer._post_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.POST, url=url+param, custom_status_code_exceptions={409: exceptions.NoteAlreadyOpen()}) @@ -244,7 +250,8 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int if sort: url += f"&sort={sort}" if order: url += f"&order={order}" - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=url, custom_status_code_exceptions={400: ValueError("Limits exceeded")}) diff --git a/src/osm_easy_api/api/endpoints/user.py b/src/osm_easy_api/api/endpoints/user.py index 5514120..1395717 100644 --- a/src/osm_easy_api/api/endpoints/user.py +++ b/src/osm_easy_api/api/endpoints/user.py @@ -63,7 +63,8 @@ def get(self, id: int) -> User: Returns: User: User object. """ - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=self.outer._url.user["get"].format(id=id)) return self._xml_to_users_list(generator)[0] @@ -81,7 +82,8 @@ def get_query(self, ids: list[int]) -> list[User]: for id in ids: param += f"{id}," param = param[:-1] - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=self.outer._url.user["get_query"] + param) return self._xml_to_users_list(generator) @@ -92,7 +94,8 @@ def get_current(self) -> User: Returns: User: User object. """ - generator = self.outer._get_generator( + generator = self.outer._request_generator( + method=self.outer._RequestMethods.GET, url=self.outer._url.user["get_current"]) return self._xml_to_users_list(generator)[0] @@ -115,7 +118,7 @@ def get_preferences(self, key: str | None = None) -> dict[str, str]: response = self.outer._request(self.outer._RequestMethods.GET, url, custom_status_code_exceptions={404: ValueError("Preference not found")}) return {key: response.text} - generator = self.outer._get_generator(url=url) + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) preferences = {} for element in generator: From 0b5a5156ef2ecc23a7ba5a36f70beca17b952d4b Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:20:48 +0100 Subject: [PATCH 04/50] sequence parameter in ElementTree.iterparse Tuples changed to arrays --- src/osm_easy_api/api/api.py | 2 +- src/osm_easy_api/diff/diff_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index 3dadf35..47784b1 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -51,7 +51,7 @@ def _request(self, method: _RequestMethods, url: str, stream: bool = False, cust @staticmethod def _raw_stream_parser(xml_raw_stream: "HTTPResponse") -> Generator[ElementTree.Element, None, None]: - iterator = ElementTree.iterparse(xml_raw_stream, events=('end', )) + iterator = ElementTree.iterparse(xml_raw_stream, events=['end']) for event, element in iterator: yield element diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index ce5a55f..8436d58 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -165,7 +165,7 @@ def OsmChange_parser_generator(file: gzip.GzipFile, sequence_number: str | None, try: file.seek(0) except: pass - iterator = ElementTree.iterparse(file, events=('start', 'end')) + iterator = ElementTree.iterparse(file, events=['start', 'end']) _, root = next(iterator) yield Meta(version=root.attrib["version"], generator=root.attrib["generator"], sequence_number=sequence_number or "") for event, element in iterator: From cc66f8f50dfc09c78d8074c68fcec9c1ece71535 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:33:05 +0100 Subject: [PATCH 05/50] fix types in `elements` endpoint --- CHANGELOG.md | 10 +++ src/osm_easy_api/api/endpoints/elements.py | 98 ++++++++++++---------- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab1f0df..ea1acd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for `oAuth2`: `access_token` parameter in `Api` class constructor. +### Fixed +- Types in `elements` endpoint. +- Missing documentation. + ### Changed - The way http errors are handled. +- In `elements.get()` endpoint the `element` parameter has been renamed to `elementType`. +- In `elements.history()` endpoint the `element` parameter has been renamed to `elementType`. +- In `elements.version()` endpoint the `element` parameter has been renamed to `elementType`. +- In `elements.getQuery()` endpoint the `element` parameter has been renamed to `elementType`. +- In `elements.relations()` endpoint the `element` parameter has been renamed to `elementType`. +- In `elements.full()` endpoint the `element` parameter has been renamed to `elementType`. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 6edf6a3..ed7c4e8 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, TypeVar, Type, cast if TYPE_CHECKING: # pragma: no cover from ...api import Api @@ -40,11 +40,11 @@ def create(self, element: Node | Way | Relation, changeset_id: int) -> int: return int(response.text) - def get(self, element: type[Node_Way_Relation], id: int) -> Node_Way_Relation : + def get(self, elementType: Type[Node_Way_Relation], id: int) -> Node_Way_Relation: """""Get element by id Args: - element (type[Node_Way_Relation]): Element type. + elementType (Type[Node_Way_Relation]): Element type. id (int): Element id. Raises: @@ -52,9 +52,9 @@ def get(self, element: type[Node_Way_Relation], id: int) -> Node_Way_Relation : exceptions.ElementDeleted: Element has been deleted. Maybe you should use elements.version() instead? Returns: - Node | Way | Relation: Representation of element. + Node_Way_Relation: Representation of element. """"" - element_name = element.__name__.lower() + element_name = elementType.__name__.lower() url = self.outer._url.elements["read"].format(element_type=element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url, @@ -63,9 +63,9 @@ def get(self, element: type[Node_Way_Relation], id: int) -> Node_Way_Relation : for elem in generator: if elem.tag in ("node", "way", "relation"): object = _element_to_osm_object(elem) - return object - - return object + return cast(elementType, object) + + assert False, "No objects to parse!" def update(self, element: Node | Way | Relation, changeset_id: int) -> int: """Updates data for existing element. @@ -81,7 +81,7 @@ def update(self, element: Node | Way | Relation, changeset_id: int) -> int: exceptions.IdNotFoundError: Way or relation has members/elements that do not exist or are not visible. Returns: - int: _description_ + int: The new version number. """ element.changeset_id = changeset_id element_name = element.__class__.__name__.lower() @@ -108,7 +108,7 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: ValueError: Node is still used in way or element is still member of relation. Returns: - int: New version number. + int: The new version number. """ element.changeset_id = changeset_id element_name = element.__class__.__name__.lower() @@ -119,20 +119,20 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: return int(response.content) # FIXME: Should be text? - def history(self, element: type[Node_Way_Relation], id: int) -> list[Node_Way_Relation]: + def history(self, elementType: Type[Node_Way_Relation], id: int) -> list[Node_Way_Relation]: """Returns all old versions of element. Args: - element (type[Node_Way_Relation]): Element type to search for. + elementType (Type[Node_Way_Relation]): Element type to search for. id (int): Element id. Raises: exceptions.IdNotFoundError: cannot find element with given id. Returns: - list[Node | Way | Relation]: List of previous versions of element. + list[Node_Way_Relation]: List of previous versions of element. """ - element_name = element.__name__.lower() + element_name = elementType.__name__.lower() url = self.outer._url.elements["history"].format(element_type=element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) @@ -143,11 +143,11 @@ def history(self, element: type[Node_Way_Relation], id: int) -> list[Node_Way_Re return objects_list - def version(self, element: type[Node_Way_Relation], id: int, version: int) -> Node_Way_Relation: + def version(self, elementType: Type[Node_Way_Relation], id: int, version: int) -> Node_Way_Relation: """Returns specific version of element. Args: - element (type[Node_Way_Relation]): Element type. + elementType (Type[Node_Way_Relation]): Element type. id (int): Element id. version (int): Version number you are looking for. @@ -156,9 +156,9 @@ def version(self, element: type[Node_Way_Relation], id: int, version: int) -> No exceptions.IdNotFoundError: Cannot find element with given id. Returns: - Node | Way | Relation: _description_ + Node_Way_Relation: Element in specific version. """ - element_name = element.__name__.lower() + element_name = elementType.__name__.lower() url = self.outer._url.elements["version"].format(element_type=element_name, id=id, version=version) generator = self.outer._request_generator( method=self.outer._RequestMethods.GET, @@ -169,14 +169,14 @@ def version(self, element: type[Node_Way_Relation], id: int, version: int) -> No for elem in generator: if elem.tag in ("node", "way", "relation"): - return _element_to_osm_object(elem) + return cast(Node_Way_Relation, _element_to_osm_object(elem)) assert False, "[ERROR::API::ENDPOINTS::ELEMENTS::version] Cannot create an element." - def get_query(self, element: type[Node_Way_Relation], ids: list[int]) -> list[Node_Way_Relation]: + def get_query(self, elementType: Type[Node_Way_Relation], ids: list[int]) -> list[Node_Way_Relation]: """Allows fetch multiple elements at once. Args: - element (type[Node | Way | Relation]): Elements type. + elementType (Type[Node_Way_Relation]): Elements type. ids (list[int]): List of ids you are looking for. Raises: @@ -185,9 +185,9 @@ def get_query(self, element: type[Node_Way_Relation], ids: list[int]) -> list[No ValueError: Request url was too long (too many ids.) Returns: - list[Node | Way | Relation]: List of elements you are looking for. + list[Node_Way_Relation]: List of elements you are looking for. """ - element_name = element.__name__.lower() + 's' + element_name = elementType.__name__.lower() + 's' param = f"?{element_name}=" for id in ids: param += f"{id}," param = param[:-1] @@ -201,22 +201,22 @@ def get_query(self, element: type[Node_Way_Relation], ids: list[int]) -> list[No objects_list = [] for elem in generator: - if elem.tag == element.__name__.lower(): + if elem.tag == elementType.__name__.lower(): objects_list.append(_element_to_osm_object(elem)) return objects_list - def relations(self, element: type[Node | Way | Relation], id: int) -> list[Relation]: + def relations(self, elementType: Type[Node | Way | Relation], id: int) -> list[Relation]: """Gets all relations that given element is in. Args: - element (type[Node | Way | Relation]): Element type. + elementType (Type[Node | Way | Relation]): Element type. id (int): Element id. Returns: list[Relation]: List of Relations that element is in. """ - element_name = element.__name__.lower() + element_name = elementType.__name__.lower() url = self.outer._url.elements["relations"].format(element_type=element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) @@ -246,11 +246,11 @@ def ways(self, node_id: int) -> list[Way]: return ways_list - def full(self, element: type[Way_Relation], id: int) -> Way_Relation: + def full(self, elementType: Type[Way_Relation], id: int) -> Way_Relation: """Retrieves a way or relation and all other elements referenced by it. See https://wiki.openstreetmap.org/wiki/API_v0.6#Full:_GET_/api/0.6/[way|relation]/#id/full for more info. Args: - element (type[Way_Relation]): Type of element. + elementType (Type[Way_Relation]): Type of element. id (int): Element id. Raises: @@ -258,9 +258,9 @@ def full(self, element: type[Way_Relation], id: int) -> Way_Relation: exceptions.ElementDeleted: Element already deleted. Returns: - Way | Relation: Way or Relation with complete data. + Way_Relation: Way or Relation with complete data. """ - element_name = element.__name__.lower() + element_name = elementType.__name__.lower() url = self.outer._url.elements["full"].format(element_type = element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) @@ -269,33 +269,41 @@ def full(self, element: type[Way_Relation], id: int) -> Way_Relation: relations_dict: dict[int, Relation] = {} for elem in generator: if elem.tag == "node": - node = _element_to_osm_object(elem) + node = cast(Node, _element_to_osm_object(elem)) + assert node.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" nodes_dict.update({node.id: node}) if elem.tag == "way": - way = _element_to_osm_object(elem) + way = cast(Way, _element_to_osm_object(elem)) + assert way.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" ways_dict.update({way.id: way}) if elem.tag == "relation" and element_name == "relation": - relation = _element_to_osm_object(elem) + relation = cast(Relation, _element_to_osm_object(elem)) + assert relation.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" relations_dict.update({relation.id: relation}) - for way_id in ways_dict: - for i in range(len(ways_dict[way_id].nodes)): - ways_dict[way_id].nodes[i] = deepcopy(nodes_dict[ways_dict[way_id].nodes[i].id]) + for way in ways_dict.values(): + for i in range(len(way.nodes)): + node_id = way.nodes[i].id + assert node_id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" + node = nodes_dict[node_id] + way.nodes[i] = deepcopy(node) if element_name == "relation": - for relation_id in relations_dict: - members = relations_dict[relation_id].members + for relation in relations_dict.values(): + members = relation.members for i in range(len(members)): - if isinstance(members[i].element, Node): - members[i] = Member(deepcopy(nodes_dict[members[i].element.id]), members[i].role) - elif isinstance(members[i].element, Way): - members[i] = Member(deepcopy(ways_dict[members[i].element.id]), members[i].role) + element = members[i].element + assert element.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {element}" + if isinstance(element, Node): + members[i] = Member(deepcopy(nodes_dict[element.id]), members[i].role) + elif isinstance(element, Way): + members[i] = Member(deepcopy(ways_dict[element.id]), members[i].role) del nodes_dict, ways_dict - return relations_dict[id] + return cast(Way_Relation, relations_dict[id]) else: del nodes_dict, relations_dict - return ways_dict[id] + return cast(Way_Relation, ways_dict[id]) def redaction(self, element: type[Node | Way | Relation], id: int, version: int, redaction_id: int) -> None: """Moderator only https://wiki.openstreetmap.org/wiki/API_v0.6#Redaction:_POST_/api/0.6/[node|way|relation]/#id/#version/redact?redaction=#redaction_id From 8e637c1dc9dcb31d15f6ac91e195e68bc2df0fa6 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:38:34 +0100 Subject: [PATCH 06/50] Fixed return type --- CHANGELOG.md | 1 + src/osm_easy_api/api/endpoints/misc.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1acd5..dca0ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Types in `elements` endpoint. - Missing documentation. +- `misc.get_map_in_bbox()` endpoint should not yield `string`. ### Changed - The way http errors are handled. diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index c7f0a93..c0b7d75 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Generator, cast if TYPE_CHECKING: # pragma: no cover from xml.etree import ElementTree from ... import Node, Way, Relation @@ -68,7 +68,7 @@ def policy_parser(dict: dict, policy_element: "ElementTree.Element") -> None: return return_dict - def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) -> Generator["Node | Way | Relation | str", None, None]: + def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) -> Generator[Node | Way | Relation, None, None]: """Returns generator of map data in border box. See https://wiki.openstreetmap.org/wiki/API_v0.6#Retrieving_map_data_by_bounding_box:_GET_/api/0.6/map for more info. Args: @@ -94,7 +94,7 @@ def generator(): gen = OsmChange_parser_generator(response.raw, None) next(gen) # for meta data for action, element in gen: # type: ignore - yield element # type: ignore + yield cast(Node | Way | Relation, element) return generator() def permissions(self) -> list: From 7d796014d9f39f7b2d6949547bf4b15e665582bd Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:41:19 +0100 Subject: [PATCH 07/50] Fix type hints --- src/osm_easy_api/api/endpoints/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index c0b7d75..f8c98e4 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -68,7 +68,7 @@ def policy_parser(dict: dict, policy_element: "ElementTree.Element") -> None: return return_dict - def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) -> Generator[Node | Way | Relation, None, None]: + def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) -> Generator["Node | Way | Relation", None, None]: """Returns generator of map data in border box. See https://wiki.openstreetmap.org/wiki/API_v0.6#Retrieving_map_data_by_bounding_box:_GET_/api/0.6/map for more info. Args: @@ -94,7 +94,7 @@ def generator(): gen = OsmChange_parser_generator(response.raw, None) next(gen) # for meta data for action, element in gen: # type: ignore - yield cast(Node | Way | Relation, element) + yield cast("Node | Way | Relation", element) return generator() def permissions(self) -> list: From 722009d6c93cc78ce768c18f50e7119b746b72f0 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:58:12 +0100 Subject: [PATCH 08/50] Fixing `# type: ignore` --- CHANGELOG.md | 1 + src/osm_easy_api/api/endpoints/changeset.py | 17 +++++------ src/osm_easy_api/diff/diff.py | 8 +++--- src/osm_easy_api/diff/diff_parser.py | 11 +++---- tests/api/test_api_changeset.py | 32 +++++++++++++++++++++ 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dca0ab9..5c2e0a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. +- Most `# type: ignore`. ## [2.2.0] ### Added diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index a89eb80..a4ca23c 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -1,6 +1,6 @@ from xml.dom import minidom -from typing import TYPE_CHECKING, Generator, Tuple +from typing import TYPE_CHECKING, Generator, Tuple, cast if TYPE_CHECKING: # pragma: no cover from xml.etree import ElementTree from ...api import Api @@ -103,7 +103,7 @@ def get(self, id: int, include_discussion: bool = False) -> Changeset: param = f"{id}?include_discussion={include_discussion_text}" generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=join_url(self.outer._url.changeset["get"], param)) - return self._xml_to_changesets_list(generator, include_discussion)[0] # type: ignore + return self._xml_to_changesets_list(generator, include_discussion)[0] def get_query(self, left: float | None = None, bottom: float | None = None, right: float | None = None, top: float | None = None, user_id: str | None = None, display_name: str | None = None, @@ -156,7 +156,7 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ url=join_url(self.outer._url.changeset["get_query"], param), custom_status_code_exceptions={400: ValueError("Invalid arguments. See https://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_/api/0.6/changesets for more info.")}) - return self._xml_to_changesets_list(generator) # type: ignore + return self._xml_to_changesets_list(generator) def update(self, id: int, comment: str | None = None, tags: Tags | None = None) -> Changeset: """Updates the changeset with new comment or tags or both. @@ -186,10 +186,10 @@ def update(self, id: int, comment: str | None = None, tags: Tags | None = None) tag_xml.setAttribute("v", comment) changeset.appendChild(tag_xml) if tags: - for tag in tags: + for key, value in tags.items(): tag_xml = root.createElement("tag") - tag_xml.setAttribute("k", tag) - tag_xml.setAttribute("v", tags.get(tag)) # type: ignore + tag_xml.setAttribute("k", key) + tag_xml.setAttribute("v", value) changeset.appendChild(tag_xml) xml.appendChild(changeset) @@ -232,8 +232,9 @@ def generator() -> Generator[tuple['Action', 'Node | Way | Relation'], None, Non gen = OsmChange_parser_generator(stream.raw, None) next(gen) # for meta data for action, element in gen: # type: ignore - assert isinstance(action, Action), "ERROR::API::ENDPOINTS::CHANGESET::download action TYPE IS NOT EQUAL TO ACTION" - yield (action, element) # type: ignore (We checked if it is Action) + action = cast('Action', action) + element = cast('Node | Way | Relation', element) + yield (action, element) return generator() def upload(self, changeset_id: int, osmChange: OsmChange, make_osmChange_valid: bool = True, work_on_copy: bool = False): diff --git a/src/osm_easy_api/diff/diff.py b/src/osm_easy_api/diff/diff.py index 1713071..692dde1 100644 --- a/src/osm_easy_api/diff/diff.py +++ b/src/osm_easy_api/diff/diff.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum import gzip -from typing import Generator +from typing import Generator, cast import requests @@ -93,9 +93,9 @@ def _return_generator_or_OsmChange(file: gzip.GzipFile, tags: Tags | str, sequen if not generator: return OsmChange_parser(file, sequence_number, tags) gen_to_return = OsmChange_parser_generator(file, sequence_number, tags) - meta = next(gen_to_return) - # FIXME type problem below - return (meta, gen_to_return) # type: ignore (First generator return will be Meta data. Idk why this is not working for typing.) + meta = cast(Meta, next(gen_to_return)) + gen_to_return = cast(Generator[tuple[Action, Node | Way | Relation], None, None], gen_to_return) + return (meta, gen_to_return) def get(self, sequence_number: str | None = None, file_to: str | None = None, file_from: str | None = None, tags: Tags | str = Tags(), generator: bool = True) -> tuple[Meta, Generator[tuple[Action, Node | Way | Relation], None, None]] | OsmChange: """Gets compressed diff file from server. diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index 8436d58..fff6ea1 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -1,5 +1,5 @@ from xml.etree import ElementTree -from typing import Generator +from typing import Generator, cast import gzip # for typing from ..data_classes import Node, Way, Relation, OsmChange, Action, Tags @@ -60,7 +60,7 @@ def _if_correct(element: ElementTree.Element, tags: Tags | str) -> bool: good_tags_count = 0 for tag in element: if tag.tag != "tag": continue - if tag.attrib["k"] in tags and tag.attrib["v"] == tags[tag.attrib["k"]]: #type: ignore (We already checked if type(tags)==Tags) + if tag.attrib["k"] in tags and tag.attrib["v"] == tags[tag.attrib["k"]]: good_tags_count += 1 return good_tags_count == len(tags) @@ -200,8 +200,9 @@ def OsmChange_parser(file: gzip.GzipFile, sequence_number: str | None, required_ # FIXME: Maybe OsmChange_parser_generator should return tuple(Meta, gen)? EDIT: I think Meta should be generated somewhere else meta = next(gen) assert type(meta) == Meta, "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER] meta type is not equal to Meta." - osmChange = OsmChange(meta.version, meta.generator, meta.sequence_number) # type: ignore + osmChange = OsmChange(meta.version, meta.generator, meta.sequence_number) for action, element in gen: # type: ignore (Next gen elements must be proper tuple type.) - assert type(action) == Action, "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER] action type is not equal to Action." - osmChange.add(element, action) # type: ignore (I just created assert for it, didn't I?) + element = cast(Node | Way | Relation, element) + action = cast(Action, action) + osmChange.add(element, action) return osmChange \ No newline at end of file diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index 42829c1..f05dfbd 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -189,6 +189,38 @@ def test_update(self): self.assertEqual(testing_changeset.tags, changeset.tags) self.assertEqual(testing_changeset.discussion, changeset.discussion) + body = """ + + + + + + + + abc + + + + + """ + responses.add(**{ + "method": responses.PUT, + "url": "https://test.pl/api/0.6/changeset/111", + "body": body, + "status": 200 + }) + testing_changeset = api.changeset.update(111, "BBB" , Tags({"testing": "no"})) + new_tags = copy(changeset.tags) + new_tags.update({"testing": "no"}) + self.assertEqual(testing_changeset.id, changeset.id) + self.assertEqual(testing_changeset.timestamp, changeset.timestamp) + self.assertEqual(testing_changeset.open, changeset.open) + self.assertEqual(testing_changeset.user_id, changeset.user_id) + self.assertEqual(testing_changeset.comments_count, changeset.comments_count) + self.assertEqual(testing_changeset.changes_count, changeset.changes_count) + self.assertEqual(testing_changeset.tags, new_tags) + self.assertEqual(testing_changeset.discussion, changeset.discussion) + def update(): return api.changeset.update(111, "BBB") responses.add(**{ From 76070a982f779eaa1fd4af66fd348189e9cc4d08 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:02:52 +0100 Subject: [PATCH 09/50] response.text for text/plain --- src/osm_easy_api/api/endpoints/elements.py | 4 ++-- tests/api/test_api_elements.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index ed7c4e8..381fb13 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -91,7 +91,7 @@ def update(self, element: Node | Way | Relation, changeset_id: int) -> int: 412: exceptions.IdNotFoundError("{TEXT}"), }) - return int(response.content) # FIXME: Should be text? + return int(response.text) def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: """Deletes element. @@ -117,7 +117,7 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: 409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor("{TEXT}") }) - return int(response.content) # FIXME: Should be text? + return int(response.text) def history(self, elementType: Type[Node_Way_Relation], id: int) -> list[Node_Way_Relation]: """Returns all old versions of element. diff --git a/tests/api/test_api_elements.py b/tests/api/test_api_elements.py index 8aef210..fd6f37d 100644 --- a/tests/api/test_api_elements.py +++ b/tests/api/test_api_elements.py @@ -138,7 +138,8 @@ def test_delete(self): api = Api("https://test.pl", LOGIN, PASSWORD) node = Node(123) - api.elements.delete(node, 333) + new_version = api.elements.delete(node, 333) + self.assertEqual(new_version, 3) @responses.activate def test_history(self): From 07009019311788a125f6ef85e5681602670a5360 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:08:04 +0100 Subject: [PATCH 10/50] tests update --- tests/api/test_api.py | 2 -- tests/api/test_api_changeset.py | 12 +++++------ tests/api/test_api_changeset_discussion.py | 10 ++++----- tests/api/test_api_elements.py | 24 +++++++++++----------- tests/api/test_api_misc.py | 2 -- tests/api/test_api_notes.py | 10 ++++----- tests/api/test_api_user.py | 12 +++++------ tests/fixtures/default_variables.py | 3 +-- 8 files changed, 35 insertions(+), 40 deletions(-) diff --git a/tests/api/test_api.py b/tests/api/test_api.py index e28175a..d0d11b9 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,5 +1,3 @@ -# TODO: Do not use LOGIN and PASSWORD in tests. - import unittest from osm_easy_api import Api diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index f05dfbd..a549ad6 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -2,7 +2,7 @@ import responses from copy import copy -from ..fixtures.default_variables import LOGIN, PASSWORD +from ..fixtures.default_variables import TOKEN from osm_easy_api import Api from osm_easy_api.data_classes import Changeset, Tags, Node @@ -19,7 +19,7 @@ def test_create(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) self.assertEqual(api.changeset.create("ABC"), 111) @responses.activate @@ -45,7 +45,7 @@ def test_get(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) changeset = Changeset( 111, "2022-12-26T13:33:40Z", @@ -96,7 +96,7 @@ def test_get_query(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) changeset = Changeset( 222, "2023-01-10T16:48:58Z", @@ -177,7 +177,7 @@ def test_update(self): [{"date": "2022-12-26T14:22:22Z", "user_id": "18179", "text": "abc"}] ) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) testing_changeset = api.changeset.update(111, "BBB") self.assertEqual(testing_changeset.id, changeset.id) @@ -255,7 +255,7 @@ def test_close(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) changeset_id = api.changeset.create("ABC") def close(): return api.changeset.close(changeset_id) diff --git a/tests/api/test_api_changeset_discussion.py b/tests/api/test_api_changeset_discussion.py index ac90229..724ff85 100644 --- a/tests/api/test_api_changeset_discussion.py +++ b/tests/api/test_api_changeset_discussion.py @@ -1,7 +1,7 @@ import unittest import responses -from ..fixtures.default_variables import LOGIN, PASSWORD +from ..fixtures.default_variables import TOKEN from osm_easy_api import Api from osm_easy_api.api import exceptions as ApiExceptions @@ -16,7 +16,7 @@ def test_comment(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) def comment(): return api.changeset.discussion.comment(111, "Hello World") comment() responses.add(**{ @@ -45,7 +45,7 @@ def test_subscribe_unsubscribe(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) def subscribe(): return api.changeset.discussion.subscribe(111) def unsubscribe(): return api.changeset.discussion.unsubscribe(111) subscribe() @@ -71,7 +71,7 @@ def test_hide(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) def hide(): return api.changeset.discussion.hide(111) hide() responses.add(**{ @@ -95,7 +95,7 @@ def test_unhide(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) def unhide(): return api.changeset.discussion.unhide(111) unhide() responses.add(**{ diff --git a/tests/api/test_api_elements.py b/tests/api/test_api_elements.py index fd6f37d..e834279 100644 --- a/tests/api/test_api_elements.py +++ b/tests/api/test_api_elements.py @@ -2,7 +2,7 @@ import responses from copy import copy -from ..fixtures.default_variables import LOGIN, PASSWORD +from ..fixtures.default_variables import TOKEN from osm_easy_api import Api from osm_easy_api.data_classes import Node, Way, Relation @@ -22,7 +22,7 @@ def test_create(self): def create_node(): return api.elements.create(node, 123) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) node = Node(latitude="123", longitude="321") self.assertEqual(create_node(), 1) @@ -74,7 +74,7 @@ def test_get(self): def get_node(): return api.elements.get(Node, 123) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) node = get_node() self.assertEqual(str(node), should_be) @@ -120,7 +120,7 @@ def test_update(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) node = api.elements.get(Node, 123) node.latitude = "1" self.assertEqual(api.elements.update(node, 1), 2) @@ -136,7 +136,7 @@ def test_delete(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) node = Node(123) new_version = api.elements.delete(node, 333) self.assertEqual(new_version, 3) @@ -161,7 +161,7 @@ def test_history(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) history = api.elements.history(Node, 123) self.assertEqual(len(history), 4) self.assertEqual(history[3].user_id, 10688) @@ -182,7 +182,7 @@ def test_version(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) version = api.elements.version(Node, 123, 4) self.assertEqual(version.user_id, 10688) @@ -205,7 +205,7 @@ def test_get_query(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) nodes = api.elements.get_query(Node, [1, 2]) self.assertEqual(nodes[0].user_id, 12342) self.assertEqual(nodes[1].user_id, 10021) @@ -237,7 +237,7 @@ def test_relations(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) relations = api.elements.relations(Way, 111) self.assertEqual(relations[0].id, 79) self.assertEqual(relations[1].members[0].role, "outer") @@ -259,7 +259,7 @@ def test_ways(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) ways = api.elements.ways(111) self.assertEqual(ways[0].id, 5638) self.assertEqual(ways[1].nodes[0].id, 1368) @@ -452,7 +452,7 @@ def test_full(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) relation = api.elements.full(Relation, 226) self.assertEqual(relation.id, 226) self.assertEqual(relation.members[1].element.id, 6178) @@ -516,6 +516,6 @@ def test_no_uid(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) history = api.elements.history(Node, 123) self.assertEqual(history[0].user_id, -1) \ No newline at end of file diff --git a/tests/api/test_api_misc.py b/tests/api/test_api_misc.py index 0a61546..9da8660 100644 --- a/tests/api/test_api_misc.py +++ b/tests/api/test_api_misc.py @@ -1,8 +1,6 @@ import unittest import responses -from ..fixtures.default_variables import LOGIN, PASSWORD - from osm_easy_api import Api from osm_easy_api.api import exceptions as ApiExceptions diff --git a/tests/api/test_api_notes.py b/tests/api/test_api_notes.py index 0792e10..27dcdb0 100644 --- a/tests/api/test_api_notes.py +++ b/tests/api/test_api_notes.py @@ -1,7 +1,7 @@ import unittest import responses -from ..fixtures.default_variables import LOGIN, PASSWORD +from ..fixtures.default_variables import TOKEN from osm_easy_api import Api from osm_easy_api.api import exceptions as ApiExceptions @@ -38,7 +38,7 @@ def test_get(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) note = api.notes.get(37970) self.assertEqual(note.id, 37970) self.assertEqual(note.longitude, "20.4660000") @@ -116,7 +116,7 @@ def test_get_bbox(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) notes = api.notes.get_bbox("20.4345", "52.2620", "20.5608", "52.2946") self.assertEqual(notes[0].id, 37970) self.assertEqual(notes[1].id, 13742) @@ -169,7 +169,7 @@ def test_create(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) note = api.notes.create("20.4345", "52.2620", "abc") self.assertEqual(note.id, 37970) @@ -203,7 +203,7 @@ def test_comment(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) note = api.notes.comment(37970, "abc") self.assertEqual(note.id, 37970) diff --git a/tests/api/test_api_user.py b/tests/api/test_api_user.py index 87db91c..369061b 100644 --- a/tests/api/test_api_user.py +++ b/tests/api/test_api_user.py @@ -1,7 +1,7 @@ import unittest import responses -from ..fixtures.default_variables import LOGIN, PASSWORD +from ..fixtures.default_variables import TOKEN from osm_easy_api import Api @@ -32,7 +32,7 @@ def test_get(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) user = api.user.get(123) self.assertEqual(user.display_name, "guggis") self.assertEqual(user.img_url, "https://www.gravatar.com/avatar/123.png") @@ -63,7 +63,7 @@ def test_query(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) user = api.user.get_query([123])[0] self.assertEqual(user.display_name, "guggis") self.assertEqual(user.img_url, "https://www.gravatar.com/avatar/123.png") @@ -91,7 +91,7 @@ def test_get_preferences(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) preferences = api.user.get_preferences() self.assertEqual(preferences, {"a": "b", "c": "d"}) preferences = api.user.get_preferences("c") @@ -106,7 +106,7 @@ def test_set_prefences(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) api.user.set_preferences({"a": "b"}) @responses.activate @@ -118,5 +118,5 @@ def test_delete_preference(self): "status": 200 }) - api = Api("https://test.pl", LOGIN, PASSWORD) + api = Api(url="https://test.pl", access_token=TOKEN) api.user.delete_preference("c") \ No newline at end of file diff --git a/tests/fixtures/default_variables.py b/tests/fixtures/default_variables.py index 9537a1d..547e289 100644 --- a/tests/fixtures/default_variables.py +++ b/tests/fixtures/default_variables.py @@ -1,2 +1 @@ -LOGIN = "abc" -PASSWORD = "123" \ No newline at end of file +TOKEN="TOKEN" \ No newline at end of file From 5366c97e209b0ba712a5d1104865ba470fabe8ad Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:15:07 +0100 Subject: [PATCH 11/50] Unauthorized and Forbidden exception --- CHANGELOG.md | 3 +++ .../api/endpoints/changeset_discussion.py | 8 ++++---- src/osm_easy_api/api/endpoints/notes.py | 7 ++----- src/osm_easy_api/api/exceptions.py | 14 +++++++++++--- tests/api/test_api_changeset_discussion.py | 4 ++-- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2e0a5..2f71c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Support for `oAuth2`: `access_token` parameter in `Api` class constructor. +- `Unauthorized` exception. (No access token.) +- `Forbidden` exception. (The access token does not support the needed scope or you must be a moderator.) ### Fixed - Types in `elements` endpoint. @@ -26,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. - Most `# type: ignore`. +- `NotAModerator` exception. It is now replaced by `Forbidden` exception. ## [2.2.0] ### Added diff --git a/src/osm_easy_api/api/endpoints/changeset_discussion.py b/src/osm_easy_api/api/endpoints/changeset_discussion.py index e70e9e7..927633b 100644 --- a/src/osm_easy_api/api/endpoints/changeset_discussion.py +++ b/src/osm_easy_api/api/endpoints/changeset_discussion.py @@ -52,10 +52,10 @@ def hide(self, comment_id: int) -> None: comment_id (int): Comment id. Raises: - exceptions.NotAModerator: You are not a moderator. + exceptions.Forbidden: You are not a moderator. exceptions.IdNotFoundError: Comment with provided id not found. """ - self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id), custom_status_code_exceptions={403: exceptions.NotAModerator()}) + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id)) def unhide(self, comment_id: int) -> None: """Set visible flag on changeset comment to true. MODERATOR ONLY! @@ -64,7 +64,7 @@ def unhide(self, comment_id: int) -> None: comment_id (int): Comment id. Raises: - exceptions.NotAModerator: You are not a moderator. + exceptions.Forbidden: You are not a moderator. exceptions.IdNotFoundError: Comment with provided id not found. """ - self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id), custom_status_code_exceptions={403: exceptions.NotAModerator()}) \ No newline at end of file + self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id)) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index ded1f52..77ba336 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -208,7 +208,7 @@ def hide(self, id: int, text: str | None = None) -> None: text (str | None, optional): Text to add as comment when hiding the note. Defaults to None. Raises: - exceptions.NotAModerator: User does not have a moderator role. + exceptions.Forbidden: User does not have a moderator role. exceptions.IdNotFoundError: Cannot find note with given id. exceptions.ElementDeleted: Note with given id has been hidden by a moderator. """ @@ -218,10 +218,7 @@ def hide(self, id: int, text: str | None = None) -> None: self.outer._request( method=self.outer._RequestMethods.DELETE, url=url+param, - stream=False, - custom_status_code_exceptions={ - 403: exceptions.NotAModerator() - } + stream=False ) def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int | None = None, from_date: str | None = None, to_date: str | None = None, sort: str = "updated_at", order: str = "newest") -> list[Note]: diff --git a/src/osm_easy_api/api/exceptions.py b/src/osm_easy_api/api/exceptions.py index 7a6758f..790b471 100644 --- a/src/osm_easy_api/api/exceptions.py +++ b/src/osm_easy_api/api/exceptions.py @@ -28,9 +28,6 @@ class AlreadySubscribed(Exception): class NotSubscribed(Exception): pass -class NotAModerator(Exception): - pass - class ElementDeleted(Exception): pass @@ -43,8 +40,19 @@ class NoteAlreadyOpen(Exception): class TooManyRequests(Exception): pass +class Unauthorized(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) + +class Forbidden(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) STATUS_CODE_EXCEPTIONS = { + 401: Unauthorized("You must provide an access token in order to use this endpoint."), + 403: Forbidden("Either the access token does not support the needed scope or you must be a moderator to use this endpoint."), 400: ValueError("{TEXT}"), 404: IdNotFoundError(), 410: ElementDeleted(), diff --git a/tests/api/test_api_changeset_discussion.py b/tests/api/test_api_changeset_discussion.py index 724ff85..4bef980 100644 --- a/tests/api/test_api_changeset_discussion.py +++ b/tests/api/test_api_changeset_discussion.py @@ -79,7 +79,7 @@ def hide(): return api.changeset.discussion.hide(111) "url": "https://test.pl/api/0.6/changeset/comment/111/hide", "status": 403 }) - self.assertRaises(ApiExceptions.NotAModerator, hide) + self.assertRaises(ApiExceptions.Forbidden, hide) responses.add(**{ "method": responses.POST, "url": "https://test.pl/api/0.6/changeset/comment/111/hide", @@ -103,7 +103,7 @@ def unhide(): return api.changeset.discussion.unhide(111) "url": "https://test.pl/api/0.6/changeset/comment/111/unhide", "status": 403 }) - self.assertRaises(ApiExceptions.NotAModerator, unhide) + self.assertRaises(ApiExceptions.Forbidden, unhide) responses.add(**{ "method": responses.POST, "url": "https://test.pl/api/0.6/changeset/comment/111/unhide", From ce4b01407812465bbd74b95e3066074cec762c90 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:18:42 +0100 Subject: [PATCH 12/50] Update api.py --- src/osm_easy_api/api/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index 47784b1..200b885 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -43,7 +43,6 @@ def _request(self, method: _RequestMethods, url: str, stream: bool = False, cust if response.status_code == 200: return response exception = custom_status_code_exceptions.get(response.status_code, None) or STATUS_CODE_EXCEPTIONS.get(response.status_code, None) - if not exception: exception = STATUS_CODE_EXCEPTIONS.get(response.status_code, None) if not exception: exception = custom_status_code_exceptions.get(-1, None) if not exception: raise NotImplementedError(f"Invalid (and unexpected) response code {response.status_code} for {url}. Please report it on GitHub.") if str(exception): raise type(exception)(str(exception).format(TEXT=response.text, CODE=response.status_code)) from exception From 6168a225a91b4a72aa2f59438258e6a27d547cf1 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:37:11 +0100 Subject: [PATCH 13/50] lookup table --- src/osm_easy_api/diff/diff_parser.py | 22 ++++++---------------- tests/diff/test_diff_parser.py | 9 +-------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index fff6ea1..21e52d5 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -6,21 +6,11 @@ from ..data_classes.relation import Member from ..data_classes.OsmChange import Meta -def _string_to_action(string: str) -> Action: - """Returns Action from string name. - - Args: - string (str): "create" | "modify" | "delete" - - Returns: - Action: If no match found returns Action.NONE. - """ - match string: - case "create": return Action.CREATE - case "modify": return Action.MODIFY - case "delete": return Action.DELETE - - case _: return Action.NONE +STRING_TO_ACTION = { + "create": Action.CREATE, + "modify": Action.MODIFY, + "delete": Action.DELETE +} def _add_members_to_relation_from_element(relation: Relation, element: ElementTree.Element) -> None: def _append_member(relation: Relation, type: type[Node | Way | Relation], member_attrib: dict) -> None: @@ -181,7 +171,7 @@ def OsmChange_parser_generator(file: gzip.GzipFile, sequence_number: str | None, # for tag in element: # if tag.tag == "tag": node_way_relation.tags.add(tag.attrib["k"], tag.attrib["v"]) - action = _string_to_action(action_string) + action = STRING_TO_ACTION.get(action_string, Action.NONE) yield(action, node_way_relation) element.clear() diff --git a/tests/diff/test_diff_parser.py b/tests/diff/test_diff_parser.py index 6c8201d..ee12b7a 100644 --- a/tests/diff/test_diff_parser.py +++ b/tests/diff/test_diff_parser.py @@ -2,18 +2,11 @@ import gzip import os -from osm_easy_api.diff.diff_parser import OsmChange_parser, _string_to_action +from osm_easy_api.diff.diff_parser import OsmChange_parser from osm_easy_api import Node, Way, Relation, Action, Tags from osm_easy_api.data_classes.relation import Member class TestDiffParser(unittest.TestCase): - def test__string_to_action(self): - self.assertEqual(_string_to_action("create"), Action.CREATE) - self.assertEqual(_string_to_action("modify"), Action.MODIFY) - self.assertEqual(_string_to_action("delete"), Action.DELETE) - self.assertEqual(_string_to_action("fwegwgew"), Action.NONE) - self.assertEqual(_string_to_action(""), Action.NONE) - def test_OsmChange_parser_basic(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") From 47d7df19be33097be5459a4e9b6d659db4fa6f2e Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:42:42 +0100 Subject: [PATCH 14/50] str to int --- CHANGELOG.md | 1 + src/osm_easy_api/api/endpoints/changeset.py | 4 ++-- tests/api/test_api_changeset.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f71c01..4cf6ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In `elements.getQuery()` endpoint the `element` parameter has been renamed to `elementType`. - In `elements.relations()` endpoint the `element` parameter has been renamed to `elementType`. - In `elements.full()` endpoint the `element` parameter has been renamed to `elementType`. +- Type of `user_id` parameter in `changeset.get_query()` was changed from `str` to `int`. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index a4ca23c..1fe97a5 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -106,7 +106,7 @@ def get(self, id: int, include_discussion: bool = False) -> Changeset: return self._xml_to_changesets_list(generator, include_discussion)[0] def get_query(self, left: float | None = None, bottom: float | None = None, right: float | None = None, top: float | None = None, - user_id: str | None = None, display_name: str | None = None, + user_id: int | None = None, display_name: str | None = None, time_one: str | None = None, time_two: str | None = None, open: bool = False, closed: bool = False, changesets_id: list[int] | None = None, @@ -119,7 +119,7 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ bottom (float | None, optional): Bottom side of bounding box (min_lat / south). Use left, bottom, right, top together. Defaults to None. right (float | None, optional): Right side of bounding box (max_lon / east). Use left, bottom, right, top together. Defaults to None. top (float | None, optional): Top side of bounding box (max_lat / north). Use left, bottom, right, top together. Defaults to None. - user_id (str | None, optional): User id. Defaults to None. + user_id (int | None, optional): User id. Defaults to None. display_name (str | None, optional): User display name. Defaults to None. time_one (str | None, optional): Find changesets closed after time_one. Defaults to None. time_two (str | None, optional): Requires time_one. Find changesets created before time_two. (Range time_one - time_two). Defaults to None. diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index a549ad6..443e192 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -107,7 +107,7 @@ def test_get_query(self): Tags({"comment": "Upload relation test"}) ) - testing_changeset = api.changeset.get_query(user_id="18179")[1] + testing_changeset = api.changeset.get_query(user_id=18179)[1] self.assertEqual(testing_changeset.id, changeset.id) self.assertEqual(testing_changeset.timestamp, changeset.timestamp) self.assertEqual(testing_changeset.open, changeset.open) @@ -129,7 +129,7 @@ def test_get_query(self): "body": body, "status": 200 }) - changeset_list = api.changeset.get_query(user_id="18179", limit=1) + changeset_list = api.changeset.get_query(user_id=18179, limit=1) self.assertEqual(changeset_list.__len__(), 1) responses.add(**{ From 71150fd7013ce7202b824c847500d31dd5fe5077 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:44:40 +0100 Subject: [PATCH 15/50] Removed brackets from if statements --- src/osm_easy_api/api/endpoints/changeset.py | 16 ++++++++-------- src/osm_easy_api/data_classes/OsmChange.py | 6 ++---- src/osm_easy_api/diff/diff.py | 6 +++--- src/osm_easy_api/diff/diff_parser.py | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 1fe97a5..049b1e6 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -136,14 +136,14 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ list[Changeset]: List of Changeset objects. """ param = "?" - if (left or bottom or right or top): param += f"bbox={left},{bottom},{right},{top}&" - if (user_id): param += f"user={user_id}&" - if (display_name): param += f"display_name={display_name}&" - if (time_one): param += f"time={time_one}&" - if (time_two): param += f",{time_two}&" - if (open): param += f"open={open}&" - if (closed): param += f"closed={closed}&" - if (changesets_id): + if left or bottom or right or top: param += f"bbox={left},{bottom},{right},{top}&" + if user_id: param += f"user={user_id}&" + if display_name: param += f"display_name={display_name}&" + if time_one: param += f"time={time_one}&" + if time_two: param += f",{time_two}&" + if open: param += f"open={open}&" + if closed: param += f"closed={closed}&" + if changesets_id: param += f"changesets={changesets_id[0]}" changesets_id.pop(0) for id in changesets_id: diff --git a/src/osm_easy_api/data_classes/OsmChange.py b/src/osm_easy_api/data_classes/OsmChange.py index e4f0fa7..cdc3581 100644 --- a/src/osm_easy_api/data_classes/OsmChange.py +++ b/src/osm_easy_api/data_classes/OsmChange.py @@ -116,10 +116,8 @@ def to_xml(self, changeset_id: int = -1, make_osmChange_valid: bool = True, work str: xml string. """ osmChange = self - if (work_on_copy): - osmChange = deepcopy(self) - if (make_osmChange_valid): - OsmChange._make_osmChange_valid(osmChange) + if work_on_copy: osmChange = deepcopy(self) + if make_osmChange_valid: OsmChange._make_osmChange_valid(osmChange) return OsmChange._to_xml(osmChange, changeset_id) diff --git a/src/osm_easy_api/diff/diff.py b/src/osm_easy_api/diff/diff.py index 692dde1..60cf60e 100644 --- a/src/osm_easy_api/diff/diff.py +++ b/src/osm_easy_api/diff/diff.py @@ -53,7 +53,7 @@ def _get_sequence_number_from_state(state_txt: str) -> str: def _get_state(self) -> str: """Downloads state.txt file content from diff server.""" - if (self.standard_url_frequency_format): url = join_url(self.url, frequency_to_str(self.frequency), "state.txt") + if self.standard_url_frequency_format: url = join_url(self.url, frequency_to_str(self.frequency), "state.txt") else: url = join_url(self.url, "state.txt") headers = {} if hasattr(self, "_user_agent"): @@ -83,7 +83,7 @@ def _build_url(url: str, frequency: Frequency | None, sequence_number: str) -> s str: Url to .osc.gz file. """ sequence_number = sequence_number.zfill(9) - if (frequency): + if frequency: return join_url(url, frequency_to_str(frequency), sequence_number[:3], sequence_number[3:6], sequence_number[6:9] + ".osc.gz") else: return join_url(url, sequence_number[:3], sequence_number[3:6], sequence_number[6:9] + ".osc.gz") @@ -117,7 +117,7 @@ def get(self, sequence_number: str | None = None, file_to: str | None = None, fi if not sequence_number: sequence_number = self.get_sequence_number() - if (self.standard_url_frequency_format): url = self._build_url(self.url, self.frequency, sequence_number) + if self.standard_url_frequency_format: url = self._build_url(self.url, self.frequency, sequence_number) else: url = self._build_url(self.url, None, sequence_number) headers = {} if hasattr(self, "_user_agent"): diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index 21e52d5..12f255c 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -164,7 +164,7 @@ def OsmChange_parser_generator(file: gzip.GzipFile, sequence_number: str | None, elif element.tag in ("node", "way", "relation"): if not element.attrib: continue - if (_if_correct(element, required_tags)): + if _if_correct(element, required_tags): node_way_relation = _element_to_osm_object(element) assert node_way_relation, "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER_GENERATOR] node_way_relation is equal to None!" From 91675c70152c8bcfd4cc067c2f5d3c1588296c74 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:21:12 +0100 Subject: [PATCH 16/50] diff_parser.py revisited --- src/osm_easy_api/diff/diff_parser.py | 145 +++++++++------------------ 1 file changed, 47 insertions(+), 98 deletions(-) diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index 12f255c..67bb74b 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -1,6 +1,7 @@ from xml.etree import ElementTree -from typing import Generator, cast -import gzip # for typing +from typing import Generator, cast, TYPE_CHECKING, TypeVar, Type +if TYPE_CHECKING: + import gzip from ..data_classes import Node, Way, Relation, OsmChange, Action, Tags from ..data_classes.relation import Member @@ -29,7 +30,7 @@ def _add_nodes_to_way_from_element(way: Way, element: ElementTree.Element) -> No way.nodes.append(Node(id=int(nd.attrib["ref"]))) -def _if_correct(element: ElementTree.Element, tags: Tags | str) -> bool: +def _is_correct(element: ElementTree.Element, tags: Tags | str) -> bool: """Checks if provided element has all required tags. Args: @@ -47,100 +48,57 @@ def _if_correct(element: ElementTree.Element, tags: Tags | str) -> bool: if tag.attrib["k"] == tags: return True return False elif type(tags) == Tags: - good_tags_count = 0 + matching_tags_counter = 0 for tag in element: if tag.tag != "tag": continue if tag.attrib["k"] in tags and tag.attrib["v"] == tags[tag.attrib["k"]]: - good_tags_count += 1 - return good_tags_count == len(tags) + matching_tags_counter += 1 + return matching_tags_counter == len(tags) - raise ValueError("[ERROR::DIFF_PARSER::_IF_CORRECT] Unexpected return.") - - # good_tags_count = 0 - # for tag in element: - # if tag.tag != "tag": continue - # if type(tags) == Tags and tag.attrib["k"] in tags and tag.attrib["v"] == tags[tag.attrib["k"]]: - # good_tags_count += 1 - # elif type(tags) == str: - # if tag.attrib["k"] == tags: - # return True - # return good_tags_count == len(tags) - -def _create_node_from_attributes(attributes: dict) -> Node: - visible = None - if attributes.get("visible"): - visible = True if attributes["visible"] == "true" else False - - user_id = -1 - if attributes.get("uid"): - user_id = int(attributes["uid"]) - return Node( - id = int( attributes["id"] ), - visible = visible, - version = int( attributes["version"] ), - timestamp = str( attributes["timestamp"] ), - user_id = user_id, - changeset_id = int( attributes["changeset"] ), - latitude = str( attributes.get("lat") ), - longitude = str( attributes.get("lon") ) - ) - -def _create_way_from_attributes(attributes: dict) -> Way: - visible = None - if attributes.get("visible"): - visible = True if attributes["visible"] == "true" else False + raise ValueError("[ERROR::DIFF_PARSER::_IS_CORRECT] Unexpected return.") - user_id = -1 - if attributes.get("uid"): - user_id = int(attributes["uid"]) - return Way( - id = int( attributes["id"] ), - visible = visible, - version = int( attributes["version"] ), - timestamp = str( attributes["timestamp"] ), - user_id = user_id, - changeset_id = int( attributes["changeset"] ) - ) - -def _create_relation_from_attributes(attributes: dict) -> Relation: + +Node_Way_Relation = TypeVar("Node_Way_Relation", Node, Way, Relation) +def _create_osm_object_from_attributes(elementType: Type[Node_Way_Relation], attributes: dict) -> Node_Way_Relation: + + id = int(attributes["id"]) visible = None if attributes.get("visible"): visible = True if attributes["visible"] == "true" else False + version = int(attributes["version"]) + timestamp = str(attributes["timestamp"]) + user_id = int(attributes.get("uid", -1)) + changeset_id = int(attributes["changeset"]) + + element = elementType(id=id, visible=visible, version=version, timestamp=timestamp, user_id=user_id, changeset_id=changeset_id) - user_id = -1 - if attributes.get("uid"): - user_id = int(attributes["uid"]) - return Relation( - id = int( attributes["id"] ), - visible = visible, - version = int( attributes["version"] ), - timestamp = str( attributes["timestamp"] ), - user_id = user_id, - changeset_id = int( attributes["changeset"] ) - ) + if type(element) == Node: + element.latitude = str(attributes.get("lat")) + element.longitude = str(attributes.get("lon")) + + return element def _element_to_osm_object(element: ElementTree.Element) -> Node | Way | Relation: def append_tags(element: ElementTree.Element, append_to: Node | Way | Relation): for tag in element: if tag.tag == "tag": append_to.tags.add(tag.attrib["k"], tag.attrib["v"]) + + osmObject = None match element.tag: - case "node": - node = _create_node_from_attributes(element.attrib) - append_tags(element, node) - return node + case "node": + osmObject = _create_osm_object_from_attributes(Node, element.attrib) case "way": - way = _create_way_from_attributes(element.attrib) - _add_nodes_to_way_from_element(way, element) - append_tags(element, way) - return way + osmObject = _create_osm_object_from_attributes(Way, element.attrib) + _add_nodes_to_way_from_element(osmObject, element) case "relation": - relation = _create_relation_from_attributes(element.attrib) - _add_members_to_relation_from_element(relation, element) - append_tags(element, relation) - return relation - case _: raise ValueError("[ERROR::DIFF_PARSER::_ELEMENT_TO_OSM_OBJECT] Unknown element tag:", element.tag) + osmObject = _create_osm_object_from_attributes(Relation, element.attrib) + _add_members_to_relation_from_element(osmObject, element) + case _: assert False, f"[ERROR::DIFF_PARSER::_ELEMENT_TO_OSM_OBJECT] Unknown element tag: {element.tag}" + + append_tags(element, osmObject) + return osmObject -def OsmChange_parser_generator(file: gzip.GzipFile, sequence_number: str | None, required_tags: Tags | str = Tags()) -> Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: +def OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: """Generator with elements in diff file. First yield will be Meta namedtuple. Args: @@ -149,33 +107,24 @@ def OsmChange_parser_generator(file: gzip.GzipFile, sequence_number: str | None, required_tags (Tags | str, optional): Useful if you want to prefetch specific tags. Other tags will be ignored. Yields: - Generator[Meta | Node | Way | Relation, None, None]: First yield will be Meta namedtuple with data about diff. Next yields will be osm data classes. + Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: First yield will be Meta namedtuple with data about diff. Next yields will be osm data classes. """ - action_string = "" + action: Action = Action.NONE try: file.seek(0) except: pass - iterator = ElementTree.iterparse(file, events=['start', 'end']) + iterator = ElementTree.iterparse(file, events=['start']) _, root = next(iterator) yield Meta(version=root.attrib["version"], generator=root.attrib["generator"], sequence_number=sequence_number or "") for event, element in iterator: - if element.tag in ("modify", "create", "delete") and event=="start": - action_string = element.tag - elif element.tag in ("node", "way", "relation"): - if not element.attrib: continue - - if _if_correct(element, required_tags): - node_way_relation = _element_to_osm_object(element) - assert node_way_relation, "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER_GENERATOR] node_way_relation is equal to None!" - - # for tag in element: - # if tag.tag == "tag": node_way_relation.tags.add(tag.attrib["k"], tag.attrib["v"]) - - action = STRING_TO_ACTION.get(action_string, Action.NONE) - yield(action, node_way_relation) + if element.tag in ("modify", "create", "delete"): + action = STRING_TO_ACTION.get(element.tag, Action.NONE) + elif element.tag in ("node", "way", "relation") and _is_correct(element, required_tags): + osmObject = _element_to_osm_object(element) + yield(action, osmObject) element.clear() -def OsmChange_parser(file: gzip.GzipFile, sequence_number: str | None, required_tags: Tags | str = Tags()) -> OsmChange: +def OsmChange_parser(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> OsmChange: """Creates OsmChange object from generator. Args: @@ -195,4 +144,4 @@ def OsmChange_parser(file: gzip.GzipFile, sequence_number: str | None, required_ element = cast(Node | Way | Relation, element) action = cast(Action, action) osmChange.add(element, action) - return osmChange \ No newline at end of file + return osmChange From c7cc84d2a0ef78265a2a390b910202d5153c22b9 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sat, 16 Mar 2024 22:05:39 +0100 Subject: [PATCH 17/50] Update README.md --- README.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index dc7e539..1ad2235 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,16 @@

Me on OpenStreetMap

-Python package for parsing osm diffs and communicating with the OpenStreetMap api. See API.txt for list of supported endpoints. +> Python package for parsing osm diffs and communicating with the OpenStreetMap api. See `API.txt` for list of supported endpoints. -## What's the point of this package? +### What's the point of this package? This package was created to provide an easy way to create automated scripts and programs that use diff and/or osm api. The main advantage is the classes (data_classes) that provide data of elements (node, way, relation, OsmChange, etc.) in a readable way and the possibility to use them in diff and api without worrying about missing data or dictionaries. You can easily find nodes in diff, add a tag to them and send the corrected version to osm. -## What next? -The plan is to optimise and improve the code, add support for gpx traces, rss support and overpass api. +### What next? +The plan is to add support for gpx traces, rss support and overpass api. -# Installation +## Installation Works on python >= 3.10. (Due to new typehints standard) @@ -33,18 +33,22 @@ Install `osm_easy_api` from [PyPi](https://pypi.org/project/osm-easy-api/): pip install osm_easy_api ``` -# Documentation +## Documentation You can view documentation on [github-pages](https://docentyt.github.io/osm_easy_api/osm_easy_api.html). Documentation is build using [pdoc](https://pdoc.dev). To run docs on your machine use preferred command: `pdoc --docformat google --no-show-source osm_easy_api !osm_easy_api.utils`. -# Examples +## OAuth 2.0 +Due to the deprecation of HTTP Basic Auth you need an access token to use most api endpoints. To obtain an access token we recommend using https://tools.interactivemaps.xyz/token/. -## DIFF -### Print trees +## Examples + +### DIFF + +#### Print trees ```py from osm_easy_api import Node, Diff, Frequency @@ -61,7 +65,7 @@ for action, element in gen: print(action, element.id) ``` -### Print incorrectly tagged single tress +#### Print incorrectly tagged single tress ```py from osm_easy_api import Diff, Frequency, Action, Node @@ -81,14 +85,14 @@ Example output: Node(id = 10208486717, visible = None, version = 1, changeset_id = 129216075, timestamp = 2022-11-22T00:16:44Z, user_id = 17471721, tags = {'leaf_type': 'broadleaved', 'natural': 'wood'}, latitude = 48.6522286, longitude = 12.583809, ) ``` -## API +### API -### Add missing wikidata tag +#### Add missing wikidata tag ```py from osm_easy_api import Api, Node, Tags -api = Api("https://master.apis.dev.openstreetmap.org", LOGIN, PASSWORD) +api = Api("https://master.apis.dev.openstreetmap.org", ACCESS_TOKEN) node = api.elements.get(Node, 4296460336) # We are getting Node with id 4296460336 where we want to add a new tag to node.tags.add("wikidata", "Qexample") # Add a new tag to node. @@ -98,7 +102,7 @@ api.elements.update(node, my_changeset) # Send new version of a node to osm api.changeset.close(my_changeset) # Close changeset. ``` -# Notes +## Notes Note that the following codes do the same thing ```py @@ -138,7 +142,7 @@ for node in deleted_nodes: ``` but it can consume large amounts of ram and use of this method is not recommended for large diff's. -# Tests +## Tests You will need to install `test-requirements.txt`. You can use tox. To run tests manually use `python -m unittest discover`. From fe89185f1c2f905d0884e8eb57586badecbd6489 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:20:42 +0100 Subject: [PATCH 18/50] Documentation update --- src/osm_easy_api/__init__.py | 1 + src/osm_easy_api/api/endpoints/__init__.py | 7 ++- src/osm_easy_api/api/endpoints/changeset.py | 32 ++++++------- .../api/endpoints/changeset_discussion.py | 21 +++------ src/osm_easy_api/api/endpoints/elements.py | 45 ++++++------------- src/osm_easy_api/api/endpoints/misc.py | 4 +- src/osm_easy_api/api/endpoints/notes.py | 9 +--- src/osm_easy_api/api/endpoints/user.py | 12 ++--- src/osm_easy_api/api/exceptions.py | 1 + src/osm_easy_api/data_classes/__init__.py | 2 +- 10 files changed, 51 insertions(+), 83 deletions(-) diff --git a/src/osm_easy_api/__init__.py b/src/osm_easy_api/__init__.py index e4d5418..84373ed 100644 --- a/src/osm_easy_api/__init__.py +++ b/src/osm_easy_api/__init__.py @@ -1,3 +1,4 @@ +"""Python package for parsing osm diffs and communicating with the OpenStreetMap api.""" VERSION = "2.2.0" from .data_classes import * diff --git a/src/osm_easy_api/api/endpoints/__init__.py b/src/osm_easy_api/api/endpoints/__init__.py index 26e24bd..7a0edb3 100644 --- a/src/osm_easy_api/api/endpoints/__init__.py +++ b/src/osm_easy_api/api/endpoints/__init__.py @@ -1,4 +1,9 @@ -"""Endpoints used to communicate with API. Use as follow: methods from changeset submodule -> `api.changeset`. (NOTE: changeset_discussion -> `api.changeset.discussion`. Not `api.changeset_discussion`) . Structure base on official API specification https://wiki.openstreetmap.org/wiki/API_v0.6 .""" +"""Endpoints used to communicate with the API. + +The module structure is based on the [official API specification](https://wiki.openstreetmap.org/wiki/API_v0.6). + +All endpoints may throw one of the exception from `osm_easy_api.api.exceptions.STATUS_CODE_EXCEPTIONS` (unless the endpoint documentation states otherwise). +""" from .misc import Misc_Container from .changeset import Changeset_Container from .elements import Elements_Container diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 049b1e6..0a250bc 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -93,9 +93,6 @@ def get(self, id: int, include_discussion: bool = False) -> Changeset: id (int): Changeset ID. include_discussion (bool, optional): Include discussion or not. Defaults to False. - Raises: - exceptions.IdNotFoundError: Raises when there is no changeset with provided ID. - Returns: Changeset: Changeset object. """ @@ -128,9 +125,8 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ changesets_id (list[int] | None, optional): List of ids to search for. Defaults to None. limit (int, optional): Specifies the maximum number of changesets returned. Must be between 1 and 100. Defaults to 100. - Raises: - ValueError: Invalid arguments. - exceptions.IdNotFoundError: user_id or display_name not found. + Custom exceptions: + - **404 -> ValueError:** Invalid arguments. Returns: list[Changeset]: List of Changeset objects. @@ -168,8 +164,9 @@ def update(self, id: int, comment: str | None = None, tags: Tags | None = None) Raises: ValueError: If no comment and tags was provided. - exceptions.IdNotFoundError: When there is no changeset with given ID. - exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: Changeset was already closed or you are not the author. + + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor`:** Changeset was already closed or you are not the author. Returns: Changeset: New Changeset object. @@ -207,9 +204,8 @@ def close(self, id: int) -> None: Args: id (int): Changeset ID. - Raises: - exceptions.IdNotFoundError: There is no changeset with given ID. - exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: The changeset was already closer or you are not the author. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor`:** Changeset was already closed or you are not the author. """ self.outer._request(self.outer._RequestMethods.PUT, self.outer._url.changeset["close"].format(id = id), custom_status_code_exceptions={409: exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor("{TEXT}")}) @@ -219,9 +215,6 @@ def download(self, id: int) -> Generator[Tuple['Action', 'Node | Way | Relation' Args: id (int): Changeset ID. - Raises: - exceptions.IdNotFoundError: There is no changeset with given ID. - Yields: Generator: Diff generator like in 'diff' module. """ @@ -246,11 +239,12 @@ def upload(self, changeset_id: int, osmChange: OsmChange, make_osmChange_valid: osmChange (OsmChange): OsmChange instance with changes you want to upload. Action cannot be empty! make_osmChange_valid (bool): - Raises: - exceptions.ErrorWhenParsingXML: Incorrect OsmChange object. Maybe missing elements attributes. - exceptions.IdNotFoundError: No changeset with provided ID or can't find element with ID in OsmChange. - exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: Changeset already closed or you are not an author. - ValueError: Unexpected but correct error. + Custom exceptions: + - **400 -> `osm_easy_api.api.exceptions.ErrorWhenParsingXML`:** Incorrect OsmChange object. Maybe missing elements attributes. + - **404 -> `osm_easy_api.api.exceptions.IdNotFoundError`:** No changeset with provided ID or can't find element with ID in OsmChange. + - **409 -> `osm_easy_api.api.exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor`:** Changeset already closed or you are not an author. + - **400 -> `osm_easy_api.api.exceptions.ErrorWhenParsingXML`:** Incorrect OsmChange object. Maybe missing elements attributes. + OTHER -> ValueError: Unexpected but correct error. """ self.outer._request( method=self.outer._RequestMethods.POST, diff --git a/src/osm_easy_api/api/endpoints/changeset_discussion.py b/src/osm_easy_api/api/endpoints/changeset_discussion.py index 927633b..726e307 100644 --- a/src/osm_easy_api/api/endpoints/changeset_discussion.py +++ b/src/osm_easy_api/api/endpoints/changeset_discussion.py @@ -17,9 +17,8 @@ def comment(self, changeset_id: int, text: str) -> None: changeset_id (int): Changeset id. text (str): The comment text. - Raises: - exceptions.ChangesetNotClosed: Changeset must be closed to add comment. - exceptions.TooManyRequests: Request has been blocked due to rate limiting. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.ChangesetNotClosed`:** Changeset must be closed to add comment. """ self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["comment"].format(id=changeset_id, text=urllib.parse.quote(text)), custom_status_code_exceptions={409: exceptions.ChangesetNotClosed()}) @@ -29,8 +28,8 @@ def subscribe(self, changeset_id: int) -> None: Args: changeset_id (int): Changeset id. - Raises: - exceptions.AlreadySubscribed: You are already subscribed to this changeset. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.AlreadySubscribed`:** You are already subscribed to this changeset. """ self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["subscribe"].format(id=changeset_id), custom_status_code_exceptions={409: exceptions.AlreadySubscribed()}) @@ -40,8 +39,8 @@ def unsubscribe(self, changeset_id: int) -> None: Args: changeset_id (int): Changeset id. - Raises: - exceptions.NotSubscribed: You are not subscribed to this changeset. + Custom exceptions: + - **404 -> `osm_easy_api.api.exceptions.NotSubscribed`:** You are not subscribed to this changeset. """ self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unsubscribe"].format(id=changeset_id), custom_status_code_exceptions={404: exceptions.NotSubscribed()}) @@ -50,10 +49,6 @@ def hide(self, comment_id: int) -> None: Args: comment_id (int): Comment id. - - Raises: - exceptions.Forbidden: You are not a moderator. - exceptions.IdNotFoundError: Comment with provided id not found. """ self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["hide"].format(comment_id=comment_id)) @@ -62,9 +57,5 @@ def unhide(self, comment_id: int) -> None: Args: comment_id (int): Comment id. - - Raises: - exceptions.Forbidden: You are not a moderator. - exceptions.IdNotFoundError: Comment with provided id not found. """ self.outer._request(self.outer._RequestMethods.POST, self.outer._url.changeset_discussion["unhide"].format(comment_id=comment_id)) \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 381fb13..8e47e86 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -23,10 +23,8 @@ def create(self, element: Node | Way | Relation, changeset_id: int) -> int: element (Node | Way | Relation): Type of element to create. changeset_id (int): Id of changeset to add to. - Raises: - ValueError: Error when creating element (bad element data) - exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: Changeset has already been closed. - ValueError: Way has nodes that do not exist or are not visible / relation has elements that do not exist or are not visible. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor`:** Changeset has already been closed. Returns: int: Id of new element. @@ -47,9 +45,8 @@ def get(self, elementType: Type[Node_Way_Relation], id: int) -> Node_Way_Relatio elementType (Type[Node_Way_Relation]): Element type. id (int): Element id. - Raises: - exceptions.IdNotFoundError: Not found element with given id and type. - exceptions.ElementDeleted: Element has been deleted. Maybe you should use elements.version() instead? + Custom exceptions: + - **410 -> `osm_easy_api.api.exceptions.ElementDeleted`:** Element has been deleted. Maybe you should use `version` instead? Returns: Node_Way_Relation: Representation of element. @@ -74,11 +71,9 @@ def update(self, element: Node | Way | Relation, changeset_id: int) -> int: element (Node | Way | Relation): Element with updated data. Version of element must be the same as in database. Unchanged data also must be provided. changeset_id (int): Changeset id in which you want to update element. - Raises: - ValueError: Error when updating element (bad element data) - ValueError: Element version does not match the current database version. - exceptions.IdNotFoundError: Cannot find element with given id. - exceptions.IdNotFoundError: Way or relation has members/elements that do not exist or are not visible. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.ElementDeleted`:** Error when updating element (bad element data) OR element version does not match the current database version. + - **412 -> `osm_easy_api.api.exceptions.IdNotFoundError`:** Way or relation has members/elements that do not exist or are not visible. Returns: int: The new version number. @@ -100,12 +95,8 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: element (Node | Way | Relation): Element object which you want to delete. Id is not sufficient. changeset_id (int): Changeset id in which you want to delete element. - Raises: - ValueError: Error when deleting object (bad element data). - exceptions.IdNotFoundError: Cannot find element with given id (element.id). - exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor: Changeset has already been closed. - exceptions.ElementDeleted: Element has already been deleted. - ValueError: Node is still used in way or element is still member of relation. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor`:** Changeset has already been closed. Returns: int: The new version number. @@ -126,9 +117,6 @@ def history(self, elementType: Type[Node_Way_Relation], id: int) -> list[Node_Wa elementType (Type[Node_Way_Relation]): Element type to search for. id (int): Element id. - Raises: - exceptions.IdNotFoundError: cannot find element with given id. - Returns: list[Node_Way_Relation]: List of previous versions of element. """ @@ -151,9 +139,8 @@ def version(self, elementType: Type[Node_Way_Relation], id: int, version: int) - id (int): Element id. version (int): Version number you are looking for. - Raises: - exceptions.IdNotFoundError: This version of the element is not available (due to redaction) - exceptions.IdNotFoundError: Cannot find element with given id. + Custom exceptions: + - **403 -> `osm_easy_api.api.exceptions.IdNotFoundError`:** This version of the element is not available (due to redaction). Returns: Node_Way_Relation: Element in specific version. @@ -179,10 +166,8 @@ def get_query(self, elementType: Type[Node_Way_Relation], ids: list[int]) -> lis elementType (Type[Node_Way_Relation]): Elements type. ids (list[int]): List of ids you are looking for. - Raises: - ValueError: Parameters missing or wrong. - exceptions.IdNotFoundError: One of the elements could not be found. - ValueError: Request url was too long (too many ids.) + Custom exceptions: + - **414 -> ValueError:** Request url was too long (too many ids). Returns: list[Node_Way_Relation]: List of elements you are looking for. @@ -253,10 +238,6 @@ def full(self, elementType: Type[Way_Relation], id: int) -> Way_Relation: elementType (Type[Way_Relation]): Type of element. id (int): Element id. - Raises: - exceptions.IdNotFoundError: Cannot find element with given id. - exceptions.ElementDeleted: Element already deleted. - Returns: Way_Relation: Way or Relation with complete data. """ diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index f8c98e4..75d2c01 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -16,7 +16,7 @@ def versions(self) -> list: """Returns API versions supported by this instance. Raises: - ValueError: [ERROR::API::MISC::versions] CAN'T FIND version + ValueError: [ERROR::API::MISC::versions] CAN'T FIND version. Returns: list: List of supported versions by instance. @@ -25,7 +25,7 @@ def versions(self) -> list: versions = [] for element in gen: if element.tag == "version": versions.append(element.text) - if len(versions) == 0: raise ValueError("[ERROR::API::MISC::versions] CAN'T FIND version") + if len(versions) == 0: raise ValueError("[ERROR::API::MISC::versions] CAN'T FIND version.") return versions def capabilities(self) -> dict: diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index 77ba336..047a643 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -206,11 +206,6 @@ def hide(self, id: int, text: str | None = None) -> None: Args: id (int): Note id. text (str | None, optional): Text to add as comment when hiding the note. Defaults to None. - - Raises: - exceptions.Forbidden: User does not have a moderator role. - exceptions.IdNotFoundError: Cannot find note with given id. - exceptions.ElementDeleted: Note with given id has been hidden by a moderator. """ url = self.outer._url.note["hide"].format(id=id, text=text) param = f"?text={text}" if text else "" @@ -234,8 +229,8 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int sort (str, optional): Which value should be used to sort notes ("updated_at" or "created_at"). Defaults to "updated_at". order (str, optional): Order of returned notes ("newset" or "oldest"). Defaults to "newest". - Raises: - ValueError: Limits exceeded. + Custom exceptions: + - **400 -> ValueError:** Limits exceeded. Returns: list[Note]: List of notes objects. diff --git a/src/osm_easy_api/api/endpoints/user.py b/src/osm_easy_api/api/endpoints/user.py index 1395717..e0a1ac2 100644 --- a/src/osm_easy_api/api/endpoints/user.py +++ b/src/osm_easy_api/api/endpoints/user.py @@ -106,8 +106,8 @@ def get_preferences(self, key: str | None = None) -> dict[str, str]: Args: key (str | None, optional): Key to search for. Defaults to None (Returns all preferences). - Raises: - ValueError: Preference not found if key was provided + Custom exceptions: + - **404 -> ValueError:** Preference not found if key was provided. Returns: dict[str, str]: Dictionary of preferences @@ -115,7 +115,7 @@ def get_preferences(self, key: str | None = None) -> dict[str, str]: url = self.outer._url.user["preferences"] if key: url += f"/{key}" - response = self.outer._request(self.outer._RequestMethods.GET, url, custom_status_code_exceptions={404: ValueError("Preference not found")}) + response = self.outer._request(self.outer._RequestMethods.GET, url, custom_status_code_exceptions={404: ValueError("Preference not found.")}) return {key: response.text} generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) @@ -151,9 +151,9 @@ def delete_preference(self, key: str) -> None: Args: key (str): Key to delete. - Raises: - ValueError: Preference not found. + Custom exceptions: + - **404 -> ValueError:** Preference not found. """ url = self.outer._url.user["preferences"] url += f"/{key}" - self.outer._request(self.outer._RequestMethods.DELETE, url, custom_status_code_exceptions={404: ValueError("Preference not found")}) \ No newline at end of file + self.outer._request(self.outer._RequestMethods.DELETE, url, custom_status_code_exceptions={404: ValueError("Preference not found.")}) \ No newline at end of file diff --git a/src/osm_easy_api/api/exceptions.py b/src/osm_easy_api/api/exceptions.py index 790b471..b6a5007 100644 --- a/src/osm_easy_api/api/exceptions.py +++ b/src/osm_easy_api/api/exceptions.py @@ -59,3 +59,4 @@ def __init__(self, message): 412: ValueError("{TEXT}"), 429: TooManyRequests(), } +"""These are all exceptions that can be reported by the API for each of `osm_easy_api.api.endpoints`, but some may override some of them according to the [official API specification](https://wiki.openstreetmap.org/wiki/API_v0.6).""" \ No newline at end of file diff --git a/src/osm_easy_api/data_classes/__init__.py b/src/osm_easy_api/data_classes/__init__.py index 9f671ae..0aec370 100644 --- a/src/osm_easy_api/data_classes/__init__.py +++ b/src/osm_easy_api/data_classes/__init__.py @@ -1,5 +1,5 @@ """Module containing classes for osm data. -If user_id is -1, it means that the user who created/edited/deleted the element no longer exists or that it was a historical anonymous edit.""" +If `user_id` is `-1`, it means that the user who created/edited/deleted the element no longer exists or that it was a historical anonymous edit.""" from .node import Node from .way import Way from .relation import Relation, Member From ca1131034071adf91dcf27da58840bdb6432cc3d Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:25:45 +0100 Subject: [PATCH 19/50] OsmChange_parser_generator and OsmChange_parser as private --- CHANGELOG.md | 1 + src/osm_easy_api/api/endpoints/changeset.py | 4 ++-- src/osm_easy_api/api/endpoints/misc.py | 4 ++-- src/osm_easy_api/diff/diff.py | 6 +++--- src/osm_easy_api/diff/diff_parser.py | 6 +++--- tests/diff/test_diff_parser.py | 16 ++++++++-------- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf6ef2..a9dcfb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In `elements.relations()` endpoint the `element` parameter has been renamed to `elementType`. - In `elements.full()` endpoint the `element` parameter has been renamed to `elementType`. - Type of `user_id` parameter in `changeset.get_query()` was changed from `str` to `int`. +- `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 0a250bc..682a61b 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -10,7 +10,7 @@ from ...utils import join_url from ...data_classes import Changeset, OsmChange from ...api import exceptions -from ...diff.diff_parser import OsmChange_parser_generator +from ...diff.diff_parser import _OsmChange_parser_generator from .changeset_discussion import Changeset_Discussion_Container @@ -222,7 +222,7 @@ def download(self, id: int) -> Generator[Tuple['Action', 'Node | Way | Relation' stream.raw.decode_content = True def generator() -> Generator[tuple['Action', 'Node | Way | Relation'], None, None]: - gen = OsmChange_parser_generator(stream.raw, None) + gen = _OsmChange_parser_generator(stream.raw, None) next(gen) # for meta data for action, element in gen: # type: ignore action = cast('Action', action) diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index 75d2c01..5bae414 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -6,7 +6,7 @@ from ...api import exceptions # TODO: Update OsmChange_parser_generator to have more general usage -from ...diff.diff_parser import OsmChange_parser_generator +from ...diff.diff_parser import _OsmChange_parser_generator class Misc_Container: def __init__(self, outer): @@ -91,7 +91,7 @@ def get_map_in_bbox(self, left: float, bottom: float, right: float, top: float) response.raw.decode_content = True def generator(): - gen = OsmChange_parser_generator(response.raw, None) + gen = _OsmChange_parser_generator(response.raw, None) next(gen) # for meta data for action, element in gen: # type: ignore yield cast("Node | Way | Relation", element) diff --git a/src/osm_easy_api/diff/diff.py b/src/osm_easy_api/diff/diff.py index 60cf60e..81f5b16 100644 --- a/src/osm_easy_api/diff/diff.py +++ b/src/osm_easy_api/diff/diff.py @@ -5,7 +5,7 @@ import requests -from .diff_parser import OsmChange_parser, OsmChange_parser_generator +from .diff_parser import _OsmChange_parser, _OsmChange_parser_generator from ..data_classes import Tags, Node, Way, Relation, OsmChange, Action from ..data_classes.OsmChange import Meta @@ -90,9 +90,9 @@ def _build_url(url: str, frequency: Frequency | None, sequence_number: str) -> s @staticmethod def _return_generator_or_OsmChange(file: gzip.GzipFile, tags: Tags | str, sequence_number: str | None, generator: bool) -> tuple[Meta, Generator[tuple[Action, Node | Way | Relation], None, None]] | OsmChange: """Returns tuple(Meta, generator) or OsmChange class depending on generator boolean.""" - if not generator: return OsmChange_parser(file, sequence_number, tags) + if not generator: return _OsmChange_parser(file, sequence_number, tags) - gen_to_return = OsmChange_parser_generator(file, sequence_number, tags) + gen_to_return = _OsmChange_parser_generator(file, sequence_number, tags) meta = cast(Meta, next(gen_to_return)) gen_to_return = cast(Generator[tuple[Action, Node | Way | Relation], None, None], gen_to_return) return (meta, gen_to_return) diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index 67bb74b..8c0fd0e 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -98,7 +98,7 @@ def append_tags(element: ElementTree.Element, append_to: Node | Way | Relation): append_tags(element, osmObject) return osmObject -def OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: +def _OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: """Generator with elements in diff file. First yield will be Meta namedtuple. Args: @@ -124,7 +124,7 @@ def OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | Non yield(action, osmObject) element.clear() -def OsmChange_parser(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> OsmChange: +def _OsmChange_parser(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> OsmChange: """Creates OsmChange object from generator. Args: @@ -135,7 +135,7 @@ def OsmChange_parser(file: "gzip.GzipFile", sequence_number: str | None, require Returns: OsmChange: osmChange object. """ - gen = OsmChange_parser_generator(file, sequence_number, required_tags) + gen = _OsmChange_parser_generator(file, sequence_number, required_tags) # FIXME: Maybe OsmChange_parser_generator should return tuple(Meta, gen)? EDIT: I think Meta should be generated somewhere else meta = next(gen) assert type(meta) == Meta, "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER] meta type is not equal to Meta." diff --git a/tests/diff/test_diff_parser.py b/tests/diff/test_diff_parser.py index ee12b7a..b5bfb65 100644 --- a/tests/diff/test_diff_parser.py +++ b/tests/diff/test_diff_parser.py @@ -2,7 +2,7 @@ import gzip import os -from osm_easy_api.diff.diff_parser import OsmChange_parser +from osm_easy_api.diff.diff_parser import _OsmChange_parser from osm_easy_api import Node, Way, Relation, Action, Tags from osm_easy_api.data_classes.relation import Member @@ -11,7 +11,7 @@ def test_OsmChange_parser_basic(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") - osmChange = OsmChange_parser(file, "-1") + osmChange = _OsmChange_parser(file, "-1") # print(osmChange) self.assertEqual(len(osmChange.get(Node, Action.CREATE )), 2 ) self.assertEqual(len(osmChange.get(Node, Action.MODIFY )), 14 ) @@ -34,7 +34,7 @@ def test_OsmChange_parser_node(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") - osmChange = OsmChange_parser(file, "-1") + osmChange = _OsmChange_parser(file, "-1") should_be = Node(id=10288507, version=8, timestamp="2022-11-12T12:08:55Z", user_id=24119, changeset_id=128810121, latitude="55.7573298", longitude="-3.8807238", tags=Tags({"railway": "switch"})) self.assertEqual(osmChange.get(Node, Action.MODIFY)[1], should_be) @@ -44,7 +44,7 @@ def test_OsmChange_parser_way(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") - osmChange = OsmChange_parser(file, "-1") + osmChange = _OsmChange_parser(file, "-1") should_be = Way(id=1112379431, version=1, timestamp="2022-11-11T21:15:26Z", user_id=10420541, changeset_id=128793616, nodes=[Node(10176911691), Node(10176911692), Node(10176911693)]) self.assertEqual(osmChange.get(Way, Action.MODIFY)[0], should_be) @@ -54,7 +54,7 @@ def test_OsmChange_parser_relation(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") - osmChange = OsmChange_parser(file, "-1") + osmChange = _OsmChange_parser(file, "-1") should_be = Relation(id=13013122, version=2, timestamp="2022-11-11T21:15:50Z", user_id=17287177, changeset_id=128793623, tags=Tags({"type": "route", "route": "bus"}), members=[Member(Node(34819782), "stop"), Member(Way(88452897), ""), Member(Way(536004622), "")]) self.assertEqual(osmChange.get(Relation, Action.CREATE)[0], should_be) self.assertEqual(osmChange.get(Relation, Action.CREATE)[0].members[0].role, "stop") @@ -65,14 +65,14 @@ def test_OsmChange_parser_tags(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") - osmChange = OsmChange_parser(file, "-1", Tags({"highway": "crossing"})) + osmChange = _OsmChange_parser(file, "-1", Tags({"highway": "crossing"})) # print(osmChange) self.assertEqual(len(osmChange.get(Node, Action.CREATE )), 2 ) self.assertEqual(len(osmChange.get(Node, Action.MODIFY )), 2 ) self.assertEqual(len(osmChange.get(Node, Action.DELETE )), 0 ) self.assertEqual(len(osmChange.get(Node, Action.NONE )), 0 ) - osmChange = OsmChange_parser(file, "-1", Tags({"highway": "crossing", "ahfuiowegwe": "afhuiweew"})) + osmChange = _OsmChange_parser(file, "-1", Tags({"highway": "crossing", "ahfuiowegwe": "afhuiweew"})) # print(osmChange) self.assertEqual(len(osmChange.get(Node, Action.CREATE )), 0 ) self.assertEqual(len(osmChange.get(Node, Action.MODIFY )), 0 ) @@ -85,7 +85,7 @@ def test_OsmChange_parser_str_tag(self): file_path = os.path.join("tests", "fixtures", "hour.xml.gz") file = gzip.open(file_path, "r") - osmChange = OsmChange_parser(file, "-1", "crossing") + osmChange = _OsmChange_parser(file, "-1", "crossing") # print(osmChange) self.assertEqual(len(osmChange.get(Node, Action.CREATE )), 1 ) self.assertEqual(len(osmChange.get(Node, Action.MODIFY )), 2 ) From e2262b8e22fa07cc97263c1d77c87423377aeb3e Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:41:52 +0100 Subject: [PATCH 20/50] coverage configuration --- .coveragerc | 10 +++++++++- src/osm_easy_api/api/api.py | 2 +- src/osm_easy_api/api/endpoints/changeset.py | 2 +- src/osm_easy_api/api/endpoints/changeset_discussion.py | 2 +- src/osm_easy_api/api/endpoints/elements.py | 2 +- src/osm_easy_api/api/endpoints/gpx.py | 2 +- src/osm_easy_api/api/endpoints/misc.py | 2 +- src/osm_easy_api/api/endpoints/notes.py | 2 +- src/osm_easy_api/api/endpoints/user.py | 2 +- 9 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.coveragerc b/.coveragerc index 112a574..54cf5b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,12 @@ source = src/osm_easy_api/* [run] -omit = tests/* \ No newline at end of file +omit = + tests/* + */__init__.py + +[report] +exclude_lines = + pragma: no cover + # This covers both typing.TYPE_CHECKING and plain TYPE_CHECKING, with any amount of whitespace + if\s+(typing\.)?TYPE_CHECKING: \ No newline at end of file diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index 200b885..b9dc660 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -3,7 +3,7 @@ from enum import Enum from typing import TYPE_CHECKING, Generator, Tuple -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from urllib3.response import HTTPResponse from requests.models import Response diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 682a61b..14d499f 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -1,7 +1,7 @@ from xml.dom import minidom from typing import TYPE_CHECKING, Generator, Tuple, cast -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from xml.etree import ElementTree from ...api import Api from ...data_classes import Node, Way, Relation diff --git a/src/osm_easy_api/api/endpoints/changeset_discussion.py b/src/osm_easy_api/api/endpoints/changeset_discussion.py index 726e307..fce9a04 100644 --- a/src/osm_easy_api/api/endpoints/changeset_discussion.py +++ b/src/osm_easy_api/api/endpoints/changeset_discussion.py @@ -1,5 +1,5 @@ from typing import TYPE_CHECKING -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from ...api import Api from ...api import exceptions diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 8e47e86..1c65d17 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -1,5 +1,5 @@ from typing import TYPE_CHECKING, TypeVar, Type, cast -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from ...api import Api from copy import deepcopy diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index 3cc2663..c9e66f8 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING import shutil -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from ...api import Api # TODO: GPX full support and parser diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index 5bae414..3c08b36 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -1,5 +1,5 @@ from typing import TYPE_CHECKING, Generator, cast -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from xml.etree import ElementTree from ... import Node, Way, Relation from ... import Api diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index 047a643..29572a2 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -1,5 +1,5 @@ from typing import TYPE_CHECKING, Generator -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from xml.etree import ElementTree from ...api import Api diff --git a/src/osm_easy_api/api/endpoints/user.py b/src/osm_easy_api/api/endpoints/user.py index e0a1ac2..6ed3d43 100644 --- a/src/osm_easy_api/api/endpoints/user.py +++ b/src/osm_easy_api/api/endpoints/user.py @@ -1,5 +1,5 @@ from typing import TYPE_CHECKING, Generator -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from xml.etree import ElementTree from ...api import Api From c1d62ed1f0769ab554e2c9e0b96f1aea24920b6d Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:54:15 +0100 Subject: [PATCH 21/50] Documentation update --- src/osm_easy_api/api/endpoints/notes.py | 28 ++++++++----------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index 29572a2..932a9d5 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -71,10 +71,6 @@ def get(self, id: int) -> Note: Args: id (int): Note id. - Raises: - exceptions.IdNotFoundError: Note with given id cannot be found. - exceptions.ElementDeleted: Note with given id has been hidden by a moderator. - Returns: Note: Note object. """ @@ -95,8 +91,8 @@ def get_bbox(self, left: str, bottom: str, right: str, top: str, limit: int = 10 limit (int, optional): Max number of notes (1 < limit < 10000). Defaults to 100. closed_days (int, optional): Number of days a note needs to be closed to no longer be returned (0 - only open, -1 - all). Defaults to 7. - Raises: - ValueError: Any of args limit is exceeded. + Custom exceptions: + - **400 -> ValueError:** Any of args limit is exceeded. Returns: list[Note]: List of notes. @@ -135,10 +131,8 @@ def comment(self, id: int, text: str) -> Note: id (int): Note id text (str): Comment text - Raises: - exceptions.IdNotFoundError: Cannot find note with given id. - exceptions.NoteAlreadyClosed: Note is closed. - exceptions.ElementDeleted: Note with given id has been hidden by a moderator. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.NoteAlreadyClosed`:** Note is closed. Returns: Note: Note object of commented note @@ -157,10 +151,8 @@ def close(self, id: int, text: str | None = None) -> Note: id (int): Note id. text (str | None, optional): Text to add as comment when closing the note. Defaults to None. - Raises: - exceptions.IdNotFoundError: Cannot find note with given id. - exceptions.NoteAlreadyClosed: Note already closed. - exceptions.ElementDeleted: Note with given id has been hidden by a moderator. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.NoteAlreadyClosed`:** Note is closed. Returns: Note: Note object of closed note. @@ -182,13 +174,11 @@ def reopen(self, id: int, text: str | None = None) -> Note: id (int): Note id. text (str | None, optional): Text to add as comment when reopening the note. Defaults to None. - Raises: - exceptions.IdNotFoundError: Cannot find note with given id. - exceptions.NoteAlreadyClosed: Note already closed. - exceptions.ElementDeleted: Note with given id has been hidden by a moderator. + Custom exceptions: + - **409 -> `osm_easy_api.api.exceptions.NoteAlreadyOpen`:** Note is open. Returns: - Note: Note object of closed note. + Note: Note object of opened note. """ url = self.outer._url.note["reopen"].format(id=id, text=text) param = f"?text={text}" if text else "" From 3a60eded79cf9d85f98bc016c03da8f3fdc9a9e7 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 15:21:35 +0100 Subject: [PATCH 22/50] notes tests and note search empty list --- src/osm_easy_api/api/endpoints/notes.py | 7 +- tests/api/test_api_notes.py | 262 ++++++++++++++---------- tests/fixtures/stubs/note_stub.py | 27 +++ 3 files changed, 179 insertions(+), 117 deletions(-) create mode 100644 tests/fixtures/stubs/note_stub.py diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index 932a9d5..fd98dc5 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -236,8 +236,5 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int method=self.outer._RequestMethods.GET, url=url, custom_status_code_exceptions={400: ValueError("Limits exceeded")}) - - try: - return self._xml_to_notes_list(generator) - except: - return [] \ No newline at end of file + + return self._xml_to_notes_list(generator) \ No newline at end of file diff --git a/tests/api/test_api_notes.py b/tests/api/test_api_notes.py index 27dcdb0..662f402 100644 --- a/tests/api/test_api_notes.py +++ b/tests/api/test_api_notes.py @@ -2,69 +2,50 @@ import responses from ..fixtures.default_variables import TOKEN +from ..fixtures.stubs import note_stub -from osm_easy_api import Api +from osm_easy_api import Api, Note from osm_easy_api.api import exceptions as ApiExceptions +def _are_notes_equal(first: Note, second: Note): + return first.to_dict() == second.to_dict() + class TestApiNotes(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.API = Api(url="https://test.pl", access_token=TOKEN) + cls.BODY = note_stub.XML_RESPONSE_BODY @responses.activate def test_get(self): - body = """ - -37970 -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970 -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/comment -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/close -2023-02-26 13:37:26 UTC -open - - -2023-02-26 13:37:26 UTC -18179 -kwiatek_123 bot -https://master.apis.dev.openstreetmap.org/user/kwiatek_123%20bot -opened -test -

test

-
-
-
-
""" + URL = "https://test.pl/api/0.6/notes/37970" + def get(): + return self.API.notes.get(note_stub.OBJECT.id or False) + responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/notes/37970", - "body": body, + "url": URL, + "body": self.BODY, "status": 200 }) + note = get() + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(_are_notes_equal(note, note_stub.OBJECT)) - api = Api(url="https://test.pl", access_token=TOKEN) - note = api.notes.get(37970) - self.assertEqual(note.id, 37970) - self.assertEqual(note.longitude, "20.4660000") - self.assertEqual(note.comments[0].text, "test") - assert note.comments[0].user, "User not exist" - self.assertEqual(note.comments[0].user.id, 18179) - - def get(): - return api.notes.get(37970) - responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/notes/37970", - "body": body, + "url": URL, + "body": self.BODY, "status": 404 }) - self.assertRaises(ApiExceptions.IdNotFoundError, get) responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/notes/37970", - "body": body, + "url": URL, + "body": self.BODY, "status": 410 }) - self.assertRaises(ApiExceptions.ElementDeleted, get) @responses.activate @@ -109,15 +90,16 @@ def test_get_bbox(self): """ + URL = "https://test.pl/api/0.6/notes?bbox=20.4345,52.2620,20.5608,52.2946&limit=100&closed=7" responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/notes?bbox=20.4345,52.2620,20.5608,52.2946&limit=100&closed=7", + "url": URL, "body": body, "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - notes = api.notes.get_bbox("20.4345", "52.2620", "20.5608", "52.2946") + notes = self.API.notes.get_bbox("20.4345", "52.2620", "20.5608", "52.2946") + self.assertTrue(responses.assert_call_count(URL, 1)) self.assertEqual(notes[0].id, 37970) self.assertEqual(notes[1].id, 13742) self.assertEqual(notes[0].comments[0].text, "test") @@ -129,109 +111,165 @@ def test_get_bbox(self): responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/notes?bbox=20.4345,52.2620,20.5608,52.2946&limit=100&closed=7", + "url": URL, "body": body, "status": 400 }) def get_bbox(): - return api.notes.get_bbox("20.4345", "52.2620", "20.5608", "52.2946") + return self.API.notes.get_bbox("20.4345", "52.2620", "20.5608", "52.2946") self.assertRaises(ValueError, get_bbox) + self.assertTrue(responses.assert_call_count(URL, 2)) @responses.activate def test_create(self): - body = """ - -37970 -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970 -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/comment -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/close -2023-02-26 13:37:26 UTC -open - - -2023-02-26 13:37:26 UTC -18179 -kwiatek_123 bot -https://master.apis.dev.openstreetmap.org/user/kwiatek_123%20bot -opened -test -

test

-
-
-
-
""" + URL = "https://test.pl/api/0.6/notes?lat=20.4345&lon=52.2620&text=test" responses.add(**{ "method": responses.POST, - "url": "https://test.pl/api/0.6/notes?lat=20.4345&lon=52.2620&text=abc", - "body": body, + "url": URL, + "body": self.BODY, "status": 200 }) - - api = Api(url="https://test.pl", access_token=TOKEN) - note = api.notes.create("20.4345", "52.2620", "abc") - self.assertEqual(note.id, 37970) + note = self.API.notes.create("20.4345", "52.2620", "test") + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(_are_notes_equal(note, note_stub.OBJECT)) @responses.activate def test_comment(self): - body = """ - -37970 -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970 -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/comment -https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/close -2023-02-26 13:37:26 UTC -open - - -2023-02-26 13:37:26 UTC -18179 -kwiatek_123 bot -https://master.apis.dev.openstreetmap.org/user/kwiatek_123%20bot -opened -test -

test

-
-
-
-
""" + URL = "https://test.pl/api/0.6/notes/37970/comment?text=test" + + def comment(): + return self.API.notes.comment(37970, "test") + responses.add(**{ "method": responses.POST, - "url": "https://test.pl/api/0.6/notes/37970/comment?text=abc", - "body": body, + "url": URL, + "body": self.BODY, "status": 200 }) - - api = Api(url="https://test.pl", access_token=TOKEN) - note = api.notes.comment(37970, "abc") - self.assertEqual(note.id, 37970) + note = comment() + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(_are_notes_equal(note, note_stub.OBJECT)) responses.add(**{ "method": responses.POST, - "url": "https://test.pl/api/0.6/notes/37970/comment?text=abc", - "body": body, + "url": URL, + "body": self.BODY, "status": 404 }) - - def comment(): - return api.notes.comment(37970, "abc") - self.assertRaises(ApiExceptions.IdNotFoundError, comment) responses.add(**{ "method": responses.POST, - "url": "https://test.pl/api/0.6/notes/37970/comment?text=abc", - "body": body, + "url": URL, + "body": self.BODY, "status": 409 }) - self.assertRaises(ApiExceptions.NoteAlreadyClosed, comment) responses.add(**{ "method": responses.POST, - "url": "https://test.pl/api/0.6/notes/37970/comment?text=abc", - "body": body, + "url": URL, + "body": self.BODY, "status": 410 }) - self.assertRaises(ApiExceptions.ElementDeleted, comment) \ No newline at end of file + self.assertRaises(ApiExceptions.ElementDeleted, comment) + + @responses.activate + def test_close(self): + URL = "https://test.pl/api/0.6/notes/37970/close?text=test" + + def close(): + return self.API.notes.close(37970, "test") + + responses.add(**{ + "method": responses.POST, + "url": URL, + "body": self.BODY, + "status": 200 + }) + note = close() + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(_are_notes_equal(note, note_stub.OBJECT)) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "body": self.BODY, + "status": 409 + }) + self.assertRaises(ApiExceptions.NoteAlreadyClosed, close) + + @responses.activate + def test_open(self): + URL = "https://test.pl/api/0.6/notes/37970/reopen?text=test" + + def reopen(): + return self.API.notes.reopen(37970, "test") + + responses.add(**{ + "method": responses.POST, + "url": URL, + "body": self.BODY, + "status": 200 + }) + note = reopen() + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(_are_notes_equal(note, note_stub.OBJECT)) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "body": self.BODY, + "status": 409 + }) + self.assertRaises(ApiExceptions.NoteAlreadyOpen, reopen) + + @responses.activate + def test_hide(self): + URL = "https://test.pl/api/0.6/notes/37970" + responses.add(**{ + "method": responses.DELETE, + "url": URL, + "status": 200 + }) + self.API.notes.hide(37970) + self.assertTrue(responses.assert_call_count(URL, 1)) + + @responses.activate + def test_search(self): + URL_1 = "https://test.pl/api/0.6/notes/search?q=test&limit=100&closed=7&sort=updated_at&order=newest" + responses.add(**{ + "method": responses.GET, + "url": URL_1, + "body": self.BODY, + "status": 200 + }) + notes = self.API.notes.search(text = "test") + self.assertTrue(responses.assert_call_count(URL_1, 1)) + self.assertTrue(_are_notes_equal(notes[0], note_stub.OBJECT)) + + URL_2 = "https://test.pl/api/0.6/notes/search?q=test&limit=9999999&closed=7&sort=updated_at&order=newest" + responses.add(**{ + "method": responses.GET, + "url": URL_2, + "body": self.BODY, + "status": 400 + }) + def search(): + return self.API.notes.search(text = "test", limit=9999999) + self.assertRaises(ValueError, search) # FIXME: We have LimitsExceeded exception in api.exceptions + self.assertTrue(responses.assert_call_count(URL_2, 1)) + + EMPTY_BODY = """ + """ + responses.add(**{ + "method": responses.GET, + "url": URL_2, + "body": EMPTY_BODY, + "status": 200 + }) + notes = search() + self.assertTrue(responses.assert_call_count(URL_2, 2)) + self.assertEqual(notes, []) diff --git a/tests/fixtures/stubs/note_stub.py b/tests/fixtures/stubs/note_stub.py new file mode 100644 index 0000000..ac289d3 --- /dev/null +++ b/tests/fixtures/stubs/note_stub.py @@ -0,0 +1,27 @@ +from osm_easy_api import Note, Comment, User + +XML_RESPONSE_BODY = """ + +37970 +https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970 +https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/comment +https://master.apis.dev.openstreetmap.org/api/0.6/notes/37970/close +2023-02-26 13:37:26 UTC +open + + +2023-02-26 13:37:26 UTC +18179 +kwiatek_123 bot +https://master.apis.dev.openstreetmap.org/user/kwiatek_123%20bot +opened +test +

test

+
+
+
+
""" + +OBJECT = Note(id=37970, latitude="52.2722000", longitude="20.4660000", note_created_at="2023-02-26 13:37:26 UTC", open=True, comments=[ + Comment(comment_created_at="2023-02-26 13:37:26 UTC", user=User(id=18179, display_name="kwiatek_123 bot"), action="opened", text="test", html="") +]) \ No newline at end of file From 2c589246bf0780d4b2ad97f92713ec70a330f4ec Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 15:29:07 +0100 Subject: [PATCH 23/50] Update test-requirements.txt --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index a23ca16..3625c31 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -responses == 0.22.0 -tox == 4.4.6 -coverage == 7.2.1 +responses == 0.25.0 +tox == 4.14.1 +coverage == 7.4.3 coverage-badge == 1.1.0 \ No newline at end of file From a401685ab09d01187b4590352f444b717fb99adc Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 15:29:34 +0100 Subject: [PATCH 24/50] Update API.txt --- API.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/API.txt b/API.txt index 31904bd..d39e71b 100644 --- a/API.txt +++ b/API.txt @@ -57,10 +57,10 @@ NOTES ✅ GET /api/0.6/notes/#id - Get note by id ✅ POST /api/0.6/notes - Create new note ✅ POST /api/0.6/notes/#id/comment - Create new comment to note -✅❓ POST /api/0.6/notes/#id/close - Close note -✅❓ POST /api/0.6/notes/#id/reopen - Reopen note -✅❓ MO: DELETE /api/0.6/notes/#id/ - Hide note. -✅❓ GET /api/0.6/notes/search - Search for notes +✅ POST /api/0.6/notes/#id/close - Close note +✅ POST /api/0.6/notes/#id/reopen - Reopen note +✅ MO: DELETE /api/0.6/notes/#id/ - Hide note. +✅ GET /api/0.6/notes/search - Search for notes X GET /api/0.6/notes/feed?bbox=Left,Bottom,Right,Top - Get RSS feed for notes in bbox From d3405bf07fe208383c95492400b31a5c2378f3d5 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 16:18:05 +0100 Subject: [PATCH 25/50] exception --- CHANGELOG.md | 3 ++- src/osm_easy_api/api/endpoints/notes.py | 4 ++-- tests/api/test_api_notes.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9dcfb0..5c7ec5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In `elements.relations()` endpoint the `element` parameter has been renamed to `elementType`. - In `elements.full()` endpoint the `element` parameter has been renamed to `elementType`. - Type of `user_id` parameter in `changeset.get_query()` was changed from `str` to `int`. -- `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. +- `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. +- `notes.search()` endpoint throws `LimitsExceeded` exception instead of `ValueError`. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index fd98dc5..63d43dc 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -220,7 +220,7 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int order (str, optional): Order of returned notes ("newset" or "oldest"). Defaults to "newest". Custom exceptions: - - **400 -> ValueError:** Limits exceeded. + - **400 -> `osm_easy_api.api.exceptions.LimitsExceeded`:** Limits exceeded. Returns: list[Note]: List of notes objects. @@ -235,6 +235,6 @@ def search(self, text: str, limit: int = 100, closed_days: int = 7, user_id: int generator = self.outer._request_generator( method=self.outer._RequestMethods.GET, url=url, - custom_status_code_exceptions={400: ValueError("Limits exceeded")}) + custom_status_code_exceptions={400: exceptions.LimitsExceeded("{TEXT}")}) return self._xml_to_notes_list(generator) \ No newline at end of file diff --git a/tests/api/test_api_notes.py b/tests/api/test_api_notes.py index 662f402..903fc4a 100644 --- a/tests/api/test_api_notes.py +++ b/tests/api/test_api_notes.py @@ -259,7 +259,7 @@ def test_search(self): }) def search(): return self.API.notes.search(text = "test", limit=9999999) - self.assertRaises(ValueError, search) # FIXME: We have LimitsExceeded exception in api.exceptions + self.assertRaises(ApiExceptions.LimitsExceeded, search) self.assertTrue(responses.assert_call_count(URL_2, 1)) EMPTY_BODY = """ From 582b3756193673f4ab01ac84526274907bf28a5c Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 16:20:28 +0100 Subject: [PATCH 26/50] elementType to element_type --- CHANGELOG.md | 12 +++---- src/osm_easy_api/api/endpoints/elements.py | 40 +++++++++++----------- src/osm_easy_api/diff/diff_parser.py | 4 +-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7ec5d..91913bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The way http errors are handled. -- In `elements.get()` endpoint the `element` parameter has been renamed to `elementType`. -- In `elements.history()` endpoint the `element` parameter has been renamed to `elementType`. -- In `elements.version()` endpoint the `element` parameter has been renamed to `elementType`. -- In `elements.getQuery()` endpoint the `element` parameter has been renamed to `elementType`. -- In `elements.relations()` endpoint the `element` parameter has been renamed to `elementType`. -- In `elements.full()` endpoint the `element` parameter has been renamed to `elementType`. +- In `elements.get()` endpoint the `element` parameter has been renamed to `element_type`. +- In `elements.history()` endpoint the `element` parameter has been renamed to `element_type`. +- In `elements.version()` endpoint the `element` parameter has been renamed to `element_type`. +- In `elements.getQuery()` endpoint the `element` parameter has been renamed to `element_type`. +- In `elements.relations()` endpoint the `element` parameter has been renamed to `element_type`. +- In `elements.full()` endpoint the `element` parameter has been renamed to `element_type`. - Type of `user_id` parameter in `changeset.get_query()` was changed from `str` to `int`. - `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. - `notes.search()` endpoint throws `LimitsExceeded` exception instead of `ValueError`. diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 1c65d17..8e590c8 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -38,11 +38,11 @@ def create(self, element: Node | Way | Relation, changeset_id: int) -> int: return int(response.text) - def get(self, elementType: Type[Node_Way_Relation], id: int) -> Node_Way_Relation: + def get(self, element_type: Type[Node_Way_Relation], id: int) -> Node_Way_Relation: """""Get element by id Args: - elementType (Type[Node_Way_Relation]): Element type. + element_type (Type[Node_Way_Relation]): Element type. id (int): Element id. Custom exceptions: @@ -51,7 +51,7 @@ def get(self, elementType: Type[Node_Way_Relation], id: int) -> Node_Way_Relatio Returns: Node_Way_Relation: Representation of element. """"" - element_name = elementType.__name__.lower() + element_name = element_type.__name__.lower() url = self.outer._url.elements["read"].format(element_type=element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url, @@ -60,7 +60,7 @@ def get(self, elementType: Type[Node_Way_Relation], id: int) -> Node_Way_Relatio for elem in generator: if elem.tag in ("node", "way", "relation"): object = _element_to_osm_object(elem) - return cast(elementType, object) + return cast(element_type, object) assert False, "No objects to parse!" @@ -110,17 +110,17 @@ def delete(self, element: Node | Way | Relation, changeset_id: int) -> int: return int(response.text) - def history(self, elementType: Type[Node_Way_Relation], id: int) -> list[Node_Way_Relation]: + def history(self, element_type: Type[Node_Way_Relation], id: int) -> list[Node_Way_Relation]: """Returns all old versions of element. Args: - elementType (Type[Node_Way_Relation]): Element type to search for. + element_type (Type[Node_Way_Relation]): Element type to search for. id (int): Element id. Returns: list[Node_Way_Relation]: List of previous versions of element. """ - element_name = elementType.__name__.lower() + element_name = element_type.__name__.lower() url = self.outer._url.elements["history"].format(element_type=element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) @@ -131,11 +131,11 @@ def history(self, elementType: Type[Node_Way_Relation], id: int) -> list[Node_Wa return objects_list - def version(self, elementType: Type[Node_Way_Relation], id: int, version: int) -> Node_Way_Relation: + def version(self, element_type: Type[Node_Way_Relation], id: int, version: int) -> Node_Way_Relation: """Returns specific version of element. Args: - elementType (Type[Node_Way_Relation]): Element type. + element_type (Type[Node_Way_Relation]): Element type. id (int): Element id. version (int): Version number you are looking for. @@ -145,7 +145,7 @@ def version(self, elementType: Type[Node_Way_Relation], id: int, version: int) - Returns: Node_Way_Relation: Element in specific version. """ - element_name = elementType.__name__.lower() + element_name = element_type.__name__.lower() url = self.outer._url.elements["version"].format(element_type=element_name, id=id, version=version) generator = self.outer._request_generator( method=self.outer._RequestMethods.GET, @@ -159,11 +159,11 @@ def version(self, elementType: Type[Node_Way_Relation], id: int, version: int) - return cast(Node_Way_Relation, _element_to_osm_object(elem)) assert False, "[ERROR::API::ENDPOINTS::ELEMENTS::version] Cannot create an element." - def get_query(self, elementType: Type[Node_Way_Relation], ids: list[int]) -> list[Node_Way_Relation]: + def get_query(self, element_type: Type[Node_Way_Relation], ids: list[int]) -> list[Node_Way_Relation]: """Allows fetch multiple elements at once. Args: - elementType (Type[Node_Way_Relation]): Elements type. + element_type (Type[Node_Way_Relation]): Elements type. ids (list[int]): List of ids you are looking for. Custom exceptions: @@ -172,7 +172,7 @@ def get_query(self, elementType: Type[Node_Way_Relation], ids: list[int]) -> lis Returns: list[Node_Way_Relation]: List of elements you are looking for. """ - element_name = elementType.__name__.lower() + 's' + element_name = element_type.__name__.lower() + 's' param = f"?{element_name}=" for id in ids: param += f"{id}," param = param[:-1] @@ -186,22 +186,22 @@ def get_query(self, elementType: Type[Node_Way_Relation], ids: list[int]) -> lis objects_list = [] for elem in generator: - if elem.tag == elementType.__name__.lower(): + if elem.tag == element_type.__name__.lower(): objects_list.append(_element_to_osm_object(elem)) return objects_list - def relations(self, elementType: Type[Node | Way | Relation], id: int) -> list[Relation]: + def relations(self, element_type: Type[Node | Way | Relation], id: int) -> list[Relation]: """Gets all relations that given element is in. Args: - elementType (Type[Node | Way | Relation]): Element type. + element_type (Type[Node | Way | Relation]): Element type. id (int): Element id. Returns: list[Relation]: List of Relations that element is in. """ - element_name = elementType.__name__.lower() + element_name = element_type.__name__.lower() url = self.outer._url.elements["relations"].format(element_type=element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) @@ -231,17 +231,17 @@ def ways(self, node_id: int) -> list[Way]: return ways_list - def full(self, elementType: Type[Way_Relation], id: int) -> Way_Relation: + def full(self, element_type: Type[Way_Relation], id: int) -> Way_Relation: """Retrieves a way or relation and all other elements referenced by it. See https://wiki.openstreetmap.org/wiki/API_v0.6#Full:_GET_/api/0.6/[way|relation]/#id/full for more info. Args: - elementType (Type[Way_Relation]): Type of element. + element_type (Type[Way_Relation]): Type of element. id (int): Element id. Returns: Way_Relation: Way or Relation with complete data. """ - element_name = elementType.__name__.lower() + element_name = element_type.__name__.lower() url = self.outer._url.elements["full"].format(element_type = element_name, id=id) generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=url) diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index 8c0fd0e..bed8ec0 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -59,7 +59,7 @@ def _is_correct(element: ElementTree.Element, tags: Tags | str) -> bool: Node_Way_Relation = TypeVar("Node_Way_Relation", Node, Way, Relation) -def _create_osm_object_from_attributes(elementType: Type[Node_Way_Relation], attributes: dict) -> Node_Way_Relation: +def _create_osm_object_from_attributes(element_type: Type[Node_Way_Relation], attributes: dict) -> Node_Way_Relation: id = int(attributes["id"]) visible = None @@ -70,7 +70,7 @@ def _create_osm_object_from_attributes(elementType: Type[Node_Way_Relation], att user_id = int(attributes.get("uid", -1)) changeset_id = int(attributes["changeset"]) - element = elementType(id=id, visible=visible, version=version, timestamp=timestamp, user_id=user_id, changeset_id=changeset_id) + element = element_type(id=id, visible=visible, version=version, timestamp=timestamp, user_id=user_id, changeset_id=changeset_id) if type(element) == Node: element.latitude = str(attributes.get("lat")) From d588aae862cda3b3997194d02f2a8031fbe14fdf Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 21:19:57 +0100 Subject: [PATCH 27/50] gpx.create() --- API.txt | 4 +- CHANGELOG.md | 2 + src/osm_easy_api/api/_URLs.py | 3 +- src/osm_easy_api/api/api.py | 4 +- src/osm_easy_api/api/endpoints/gpx.py | 40 +- tests/api/test_api_gpx.py | 59 + tests/fixtures/gps_points.gpx | 15065 ++++++++++++++++++++++++ 7 files changed, 15167 insertions(+), 10 deletions(-) create mode 100644 tests/api/test_api_gpx.py create mode 100644 tests/fixtures/gps_points.gpx diff --git a/API.txt b/API.txt index d39e71b..b738534 100644 --- a/API.txt +++ b/API.txt @@ -36,8 +36,8 @@ ELEMENTS ✅ ✅ MO: POST /api/0.6/[node|way|relation]/#id/#version/redact?redaction=#redaction_id - Hide element with data privacy or copyright infringements. GPS TRACES -✅❓ GET /api/0.6/trackpoints?bbox=left,bottom,right,top&page=pageNumber - Get GPS tracks that are inside a given bbox -POST /api/0.6/gpx/create - Create new GPS trace from GPX file +✅ GET /api/0.6/trackpoints?bbox=left,bottom,right,top&page=pageNumber - Get GPS tracks that are inside a given bbox +✅ POST /api/0.6/gpx/create - Create new GPS trace from GPX file OO: PUT /api/0.6/gpx/#id - Update gps trace OO: DELETE /api/0.6/gpx/#id - Delete gps trace GET /api/0.6/gpx/#id/details - Get osm data about GPS trace OO: if private diff --git a/CHANGELOG.md b/CHANGELOG.md index 91913bb..c95f0c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for `oAuth2`: `access_token` parameter in `Api` class constructor. - `Unauthorized` exception. (No access token.) - `Forbidden` exception. (The access token does not support the needed scope or you must be a moderator.) +- `gpx.create()` endpoint. ### Fixed - Types in `elements` endpoint. @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Type of `user_id` parameter in `changeset.get_query()` was changed from `str` to `int`. - `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. - `notes.search()` endpoint throws `LimitsExceeded` exception instead of `ValueError`. +- `page_number` paremeter in `gpx.get()` has now default value 0. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/_URLs.py b/src/osm_easy_api/api/_URLs.py index cbfa34a..dbdc4b4 100644 --- a/src/osm_easy_api/api/_URLs.py +++ b/src/osm_easy_api/api/_URLs.py @@ -47,7 +47,8 @@ def __init__(self, base_url: str): } self.gpx: Dict[str, str] = { - "get": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}" + "get": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}", + "create": six_url + "/gpx/create" } self.user: Dict[str, str] = { diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index b9dc660..37bb842 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -38,8 +38,8 @@ def __init__(self, url: str = "https://master.apis.dev.openstreetmap.org", acces if user_agent: self._headers.update({"User-Agent": user_agent}) - def _request(self, method: _RequestMethods, url: str, stream: bool = False, custom_status_code_exceptions: dict = {int: Exception}, body = None) -> "Response": - response = requests.request(str(method), url, stream=stream, data=body.encode('utf-8') if body else None, headers=self._headers) + def _request(self, method: _RequestMethods, url: str, stream: bool = False, files: dict | None = None, custom_status_code_exceptions: dict = {int: Exception}, body = None) -> "Response": + response = requests.request(str(method), url, stream=stream, files=files, data=body.encode('utf-8') if body else None, headers=self._headers) if response.status_code == 200: return response exception = custom_status_code_exceptions.get(response.status_code, None) or STATUS_CODE_EXCEPTIONS.get(response.status_code, None) diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index c9e66f8..2cc996e 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -1,5 +1,6 @@ -from typing import TYPE_CHECKING import shutil + +from typing import TYPE_CHECKING if TYPE_CHECKING: from ...api import Api @@ -9,8 +10,8 @@ class Gpx_Container: def __init__(self, outer): self.outer: "Api" = outer - def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_number: int) -> None: - """Downloads gps points to file + def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_number: int = 0) -> None: + """Downloads gps points to file. Args: file_to (str): Path where you want to save gpx @@ -18,8 +19,37 @@ def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_n bottom (int): Bounding box right (int): Bounding box top (int): Bounding box - page_number (int): Which group of 5 000 points you want to get. + page_number (int, optional): Which group of 5 000 points you want to get. Indexed from 0. Defaults to 0. """ response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), stream=True) with open(file_to, "wb") as f_to: - shutil.copyfileobj(response.raw, f_to) \ No newline at end of file + shutil.copyfileobj(response.raw, f_to) + + def create(self, file_from: str, description: str, visibility: str, tags: list[str] | None = None) -> int: + """Uploads a GPX file or archive of GPX files. + + Args: + file_from (str): Path to file to be uploaded. + description (str): The trace description. + visibility (str): 'private', 'public', 'trackable' or 'identifiable'. See https://wiki.openstreetmap.org/wiki/Visibility_of_GPS_traces for more info. + tags (Tags | None, optional): Tags for the trace. Defaults to None. + + Returns: + int: ID of the new trace. + """ + # TODO: Enum instead of visibility string + with open(file_from, "rb") as f: + tags_string = None + if tags: + for i in range(tags.__len__()): + tags[i] = tags[i] + tags_string = ','.join(tags) + files = { + "file": f, + "description": (None, description), + "tags": (None, tags_string), + "visibility": (None, visibility) + } + + response = self.outer._request(self.outer._RequestMethods.POST, url=self.outer._url.gpx["create"], files=files) + return int(response.text) \ No newline at end of file diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py new file mode 100644 index 0000000..fabc1dc --- /dev/null +++ b/tests/api/test_api_gpx.py @@ -0,0 +1,59 @@ +import unittest +import responses +from responses.matchers import multipart_matcher +import os +import filecmp + +from ..fixtures.default_variables import TOKEN + +from osm_easy_api import Api +from osm_easy_api.api import exceptions as ApiExceptions + + +class TestApiGpx(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.API = Api(url="https://test.pl", access_token=TOKEN) + + @responses.activate + def test_get(self): + URL = "https://test.pl/api/0.6/trackpoints?bbox=10,20,30,40&page=1" + F_FROM_PATH = os.path.join("tests", "fixtures", "gps_points.gpx") + F_TO_PATH = os.path.join("tests", "fixtures", "write_gps_points.gpx") + + with open(F_FROM_PATH, "rb") as BODY: + responses.add(**{ + "method": responses.GET, + "url": URL, + "body": BODY, + "status": 200 + }) + self.API.gpx.get(F_TO_PATH, "10", "20", "30", "40", 1) + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(filecmp.cmp(F_FROM_PATH, F_TO_PATH, shallow=False)) + os.remove(F_TO_PATH) + + @responses.activate + def test_create(self): + URL = "https://test.pl/api/0.6/gpx/create" + F_FROM_PATH = os.path.join("tests", "fixtures", "gps_points.gpx") + + with open(F_FROM_PATH, "rb") as f: + matcher = multipart_matcher( + {"file": f, + "description": (None, "desc"), + "tags": (None, "a,b"), + "visibility": (None, "visib") + }, + ) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "body": "1234", + "status": 200, + "match": [matcher] + }) + ID = self.API.gpx.create(F_FROM_PATH, "desc", "visib", ["a", "b"]) + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertEqual(ID, 1234) \ No newline at end of file diff --git a/tests/fixtures/gps_points.gpx b/tests/fixtures/gps_points.gpx new file mode 100644 index 0000000..6240435 --- /dev/null +++ b/tests/fixtures/gps_points.gpx @@ -0,0 +1,15065 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20231127180206.gpx + Daily routes + /user/AmOosm/traces/11276808 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20231127081350.gpx + Daily routes + /user/AmOosm/traces/11276805 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20231126103258.gpx + Daily routes + /user/AmOosm/traces/11276793 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nocny_spacer_przez_Wisle_i_Park.gpx + Nocny spacer przez Wisle i Park + /user/GanderPL/traces/11217138 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2023_10_09T09_34_59.283419Z.gpx + Routes from sunnypilot 0.9.4.1-release (TOYOTA HIGHLANDER HYBRID 2020). + /user/sunnypilot/traces/10907732 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + swietokrzyska_droga_sw_jakuba.gpx + Świętokrzyska Droga św. Jakuba + /user/europa_caminonetpl/traces/8735777 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2023_06_25T14_50_28.000000Z.gpx + Small path bridging with the east road, forming a fork + +via StreetComplete 53.1 + /user/NyaFango/traces/8318634 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2023_06_08T09_45_48.530193Z.gpx + Routes from sunnypilot 2022.11.13 (TOYOTA HIGHLANDER HYBRID 2020). + /user/sunnypilot/traces/7870041 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e33ca3ae1aeae0c8267e3a4f0e7e18c2b50d4985 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 22:51:55 +0100 Subject: [PATCH 28/50] gpx.update() and GpxFile --- CHANGELOG.md | 3 ++ src/osm_easy_api/api/_URLs.py | 3 +- src/osm_easy_api/api/endpoints/gpx.py | 27 +++++++++-- src/osm_easy_api/data_classes/GpxFile.py | 57 +++++++++++++++++++++++ src/osm_easy_api/data_classes/__init__.py | 4 +- tests/api/test_api_gpx.py | 21 +++++++-- 6 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/osm_easy_api/data_classes/GpxFile.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c95f0c8..8428daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Unauthorized` exception. (No access token.) - `Forbidden` exception. (The access token does not support the needed scope or you must be a moderator.) - `gpx.create()` endpoint. +- `GpxFile` data class. +- `Visibility` enum. +- `gpx.update()` endpoint. ### Fixed - Types in `elements` endpoint. diff --git a/src/osm_easy_api/api/_URLs.py b/src/osm_easy_api/api/_URLs.py index dbdc4b4..9f89b38 100644 --- a/src/osm_easy_api/api/_URLs.py +++ b/src/osm_easy_api/api/_URLs.py @@ -48,7 +48,8 @@ def __init__(self, base_url: str): self.gpx: Dict[str, str] = { "get": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}", - "create": six_url + "/gpx/create" + "create": six_url + "/gpx/create", + "update": six_url + "/gpx/{id}" } self.user: Dict[str, str] = { diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index 2cc996e..d3ef037 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -1,9 +1,12 @@ import shutil from typing import TYPE_CHECKING +from xml.dom import minidom + if TYPE_CHECKING: from ...api import Api +from ...data_classes import GpxFile, Visibility # TODO: GPX full support and parser class Gpx_Container: @@ -25,13 +28,13 @@ def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_n with open(file_to, "wb") as f_to: shutil.copyfileobj(response.raw, f_to) - def create(self, file_from: str, description: str, visibility: str, tags: list[str] | None = None) -> int: + def create(self, file_from: str, description: str, visibility: Visibility, tags: list[str] | None = None) -> int: """Uploads a GPX file or archive of GPX files. Args: file_from (str): Path to file to be uploaded. description (str): The trace description. - visibility (str): 'private', 'public', 'trackable' or 'identifiable'. See https://wiki.openstreetmap.org/wiki/Visibility_of_GPS_traces for more info. + visibility (Visibility): See https://wiki.openstreetmap.org/wiki/Visibility_of_GPS_traces for more info. tags (Tags | None, optional): Tags for the trace. Defaults to None. Returns: @@ -48,8 +51,22 @@ def create(self, file_from: str, description: str, visibility: str, tags: list[s "file": f, "description": (None, description), "tags": (None, tags_string), - "visibility": (None, visibility) + "visibility": (None, visibility.value) } - response = self.outer._request(self.outer._RequestMethods.POST, url=self.outer._url.gpx["create"], files=files) - return int(response.text) \ No newline at end of file + response = self.outer._request(method=self.outer._RequestMethods.POST, url=self.outer._url.gpx["create"], files=files) + return int(response.text) + + def update(self, gpx_file: GpxFile) -> None: + """Updates a GPX file. + + Args: + gpx_file (GpxFile): GPX file with a new description and/or tags. + """ + root = minidom.Document() + xml = root.createElement("osm") + root.appendChild(xml) + xml.appendChild(gpx_file._to_xml()) + xml_str = root.toprettyxml() + + self.outer._request(method=self.outer._RequestMethods.PUT, url=self.outer._url.gpx["update"].format(id=gpx_file.id), body=xml_str) \ No newline at end of file diff --git a/src/osm_easy_api/data_classes/GpxFile.py b/src/osm_easy_api/data_classes/GpxFile.py new file mode 100644 index 0000000..3d84643 --- /dev/null +++ b/src/osm_easy_api/data_classes/GpxFile.py @@ -0,0 +1,57 @@ +from enum import Enum +from xml.dom import minidom +from dataclasses import dataclass + + + +class Visibility(Enum): + """See https://wiki.openstreetmap.org/wiki/Visibility_of_GPS_traces for values meaning.""" + IDENTIFIABLE = "identifiable" + PUBLIC = "public" + TRACKABLE = "trackable" + PRIVATE = "private" + +@dataclass +class GpxFile(): + id: int + name: str + user_id: int + visibility: Visibility + pending: bool + timestamp: str + latitude: str + longitude: str + description: str + tags: list[str] + + def __str__(self): + temp = f"{self.__class__.__name__}(" + for k in self.__dict__: + temp += f"{k} = {getattr(self, k)}, " + temp += ")" + return temp + + def _to_xml(self) -> minidom.Element: + root = minidom.Document() + gpx_file = root.createElement("gpx_file") + gpx_file.setAttribute("id", str(self.id)) + gpx_file.setAttribute("name", self.name) + gpx_file.setAttribute("uid", str(self.user_id)) + gpx_file.setAttribute("visibility", self.visibility.value) + gpx_file.setAttribute("pending", str.lower(str(self.pending))) + gpx_file.setAttribute("timestamp", self.timestamp) + gpx_file.setAttribute("lat", self.latitude) + gpx_file.setAttribute("lon", self.longitude) + + description = root.createElement("description") + description_text_node = root.createTextNode(self.description) + description.appendChild(description_text_node) + gpx_file.appendChild(description) + + for tag in self.tags: + tag_node = root.createElement("tag") + tag_text_node = root.createTextNode(tag) + tag_node.appendChild(tag_text_node) + gpx_file.appendChild(tag_node) + + return gpx_file \ No newline at end of file diff --git a/src/osm_easy_api/data_classes/__init__.py b/src/osm_easy_api/data_classes/__init__.py index 0aec370..890a9ba 100644 --- a/src/osm_easy_api/data_classes/__init__.py +++ b/src/osm_easy_api/data_classes/__init__.py @@ -8,4 +8,6 @@ from .changeset import Changeset from .user import User -from .note import Note, Comment \ No newline at end of file +from .note import Note, Comment + +from .GpxFile import GpxFile, Visibility \ No newline at end of file diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index fabc1dc..30a5c04 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -4,6 +4,8 @@ import os import filecmp +from osm_easy_api.data_classes import GpxFile, Visibility + from ..fixtures.default_variables import TOKEN from osm_easy_api import Api @@ -43,7 +45,7 @@ def test_create(self): {"file": f, "description": (None, "desc"), "tags": (None, "a,b"), - "visibility": (None, "visib") + "visibility": (None, Visibility.PRIVATE.value) }, ) @@ -54,6 +56,19 @@ def test_create(self): "status": 200, "match": [matcher] }) - ID = self.API.gpx.create(F_FROM_PATH, "desc", "visib", ["a", "b"]) + ID = self.API.gpx.create(F_FROM_PATH, "desc", Visibility.PRIVATE, ["a", "b"]) self.assertTrue(responses.assert_call_count(URL, 1)) - self.assertEqual(ID, 1234) \ No newline at end of file + self.assertEqual(ID, 1234) + + @responses.activate + def test_update(self): + URL = "https://test.pl/api/0.6/gpx/123" + GPX_FILE = GpxFile(123, "aa", 123, Visibility.PUBLIC, False, "XXX", "lat", "lon", "desc", []) + + responses.add(**{ + "method": responses.PUT, + "url": URL, + "status": 200, + }) + self.API.gpx.update(GPX_FILE) + self.assertTrue(responses.assert_call_count(URL, 1)) \ No newline at end of file From d4ff4c802e3bab2658141c98a943b7392b36af05 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:06:37 +0100 Subject: [PATCH 29/50] Better imports --- CHANGELOG.md | 1 + README.md | 17 +++++++++++------ src/osm_easy_api/__init__.py | 7 ++++--- src/osm_easy_api/api/endpoints/changeset.py | 3 +-- src/osm_easy_api/api/endpoints/misc.py | 4 ++-- tests/api/test_api.py | 2 +- tests/api/test_api_changeset.py | 2 +- tests/api/test_api_changeset_discussion.py | 2 +- tests/api/test_api_elements.py | 2 +- tests/api/test_api_gpx.py | 3 +-- tests/api/test_api_misc.py | 2 +- tests/api/test_api_notes.py | 3 ++- tests/api/test_api_user.py | 2 +- tests/data_classes/test_node.py | 2 +- tests/data_classes/test_osmChange.py | 2 +- tests/data_classes/test_osmObjectPrimitive.py | 2 +- tests/data_classes/test_relation.py | 2 +- tests/data_classes/test_tags.py | 2 +- tests/data_classes/test_way.py | 2 +- tests/diff/test_diff_parser.py | 2 +- tests/fixtures/sample_dataclasses.py | 2 +- tests/fixtures/stubs/note_stub.py | 2 +- 22 files changed, 37 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8428daf..1d1f99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. - `notes.search()` endpoint throws `LimitsExceeded` exception instead of `ValueError`. - `page_number` paremeter in `gpx.get()` has now default value 0. +- Now classes are imported from individual modules - not from the main library. See examples in the `README.md`. ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/README.md b/README.md index 1ad2235..710edc2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ Due to the deprecation of HTTP Basic Auth you need an access token to use most a #### Print trees ```py -from osm_easy_api import Node, Diff, Frequency +from osm_easy_api.diff import Diff, Frequency +from osm_easy_api.data_classes import Node # Download diff from last hour. d = Diff(Frequency.HOUR) @@ -68,7 +69,8 @@ for action, element in gen: #### Print incorrectly tagged single tress ```py -from osm_easy_api import Diff, Frequency, Action, Node +from osm_easy_api.diff import Diff, Frequency +from osm_easy_api.data_classes import Action, Node d = Diff(Frequency.DAY) @@ -90,7 +92,8 @@ Node(id = 10208486717, visible = None, version = 1, changeset_id = 129216075, ti #### Add missing wikidata tag ```py -from osm_easy_api import Api, Node, Tags +from osm_easy_api.api import Api +from osm_easy_api.data_classes import Node, Tags api = Api("https://master.apis.dev.openstreetmap.org", ACCESS_TOKEN) @@ -106,7 +109,7 @@ api.changeset.close(my_changeset) # Close changeset. Note that the following codes do the same thing ```py -from osm_easy_api import Diff, Frequency +from osm_easy_api.diff import Diff, Frequency d = Diff(Frequency.DAY) @@ -117,7 +120,8 @@ for action, element in gen: print(element) ``` ```py -from osm_easy_api import Diff, Frequency, Tags +from osm_easy_api.diff import Diff, Frequency +from osm_easy_api.data_classes import Tags d = Diff(Frequency.DAY) @@ -130,7 +134,8 @@ but the second seems to be faster. Also you can use OsmChange object if you don't want to use generator ```py -from osm_easy_api import Diff, Frequency, Action, Node +from osm_easy_api.diff import Diff, Frequency +from osm_easy_api.data_classes import Action, Node d = Diff(Frequency.MINUTE) diff --git a/src/osm_easy_api/__init__.py b/src/osm_easy_api/__init__.py index 84373ed..c3624af 100644 --- a/src/osm_easy_api/__init__.py +++ b/src/osm_easy_api/__init__.py @@ -1,7 +1,8 @@ """Python package for parsing osm diffs and communicating with the OpenStreetMap api.""" VERSION = "2.2.0" -from .data_classes import * -from .diff import Diff, Frequency +# from .data_classes import * +from . import data_classes +from . import diff -from .api import Api \ No newline at end of file +from . import api \ No newline at end of file diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 14d499f..5564917 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -6,9 +6,8 @@ from ...api import Api from ...data_classes import Node, Way, Relation -from ... import Tags, Action from ...utils import join_url -from ...data_classes import Changeset, OsmChange +from ...data_classes import Changeset, OsmChange, Tags, Action from ...api import exceptions from ...diff.diff_parser import _OsmChange_parser_generator diff --git a/src/osm_easy_api/api/endpoints/misc.py b/src/osm_easy_api/api/endpoints/misc.py index 3c08b36..1a113a9 100644 --- a/src/osm_easy_api/api/endpoints/misc.py +++ b/src/osm_easy_api/api/endpoints/misc.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, Generator, cast if TYPE_CHECKING: from xml.etree import ElementTree - from ... import Node, Way, Relation - from ... import Api + from ...data_classes import Node, Way, Relation + from ...api import Api from ...api import exceptions # TODO: Update OsmChange_parser_generator to have more general usage diff --git a/tests/api/test_api.py b/tests/api/test_api.py index d0d11b9..5992347 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,6 +1,6 @@ import unittest -from osm_easy_api import Api +from osm_easy_api.api import Api class TestApi(unittest.TestCase): def test_initialize(self): diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index 443e192..1cd98b1 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -4,7 +4,7 @@ from ..fixtures.default_variables import TOKEN -from osm_easy_api import Api +from osm_easy_api.api import Api from osm_easy_api.data_classes import Changeset, Tags, Node from osm_easy_api.api import exceptions as ApiExceptions diff --git a/tests/api/test_api_changeset_discussion.py b/tests/api/test_api_changeset_discussion.py index 4bef980..bc3e1d3 100644 --- a/tests/api/test_api_changeset_discussion.py +++ b/tests/api/test_api_changeset_discussion.py @@ -3,7 +3,7 @@ from ..fixtures.default_variables import TOKEN -from osm_easy_api import Api +from osm_easy_api.api import Api from osm_easy_api.api import exceptions as ApiExceptions class TestApiChangesetDiscussion(unittest.TestCase): diff --git a/tests/api/test_api_elements.py b/tests/api/test_api_elements.py index e834279..85742be 100644 --- a/tests/api/test_api_elements.py +++ b/tests/api/test_api_elements.py @@ -4,7 +4,7 @@ from ..fixtures.default_variables import TOKEN -from osm_easy_api import Api +from osm_easy_api.api import Api from osm_easy_api.data_classes import Node, Way, Relation from osm_easy_api.api import exceptions as ApiExceptions diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index 30a5c04..3e0ac7c 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -8,8 +8,7 @@ from ..fixtures.default_variables import TOKEN -from osm_easy_api import Api -from osm_easy_api.api import exceptions as ApiExceptions +from osm_easy_api.api import Api class TestApiGpx(unittest.TestCase): diff --git a/tests/api/test_api_misc.py b/tests/api/test_api_misc.py index 9da8660..ade5154 100644 --- a/tests/api/test_api_misc.py +++ b/tests/api/test_api_misc.py @@ -1,7 +1,7 @@ import unittest import responses -from osm_easy_api import Api +from osm_easy_api.api import Api from osm_easy_api.api import exceptions as ApiExceptions class TestApi(unittest.TestCase): diff --git a/tests/api/test_api_notes.py b/tests/api/test_api_notes.py index 903fc4a..ca7f53c 100644 --- a/tests/api/test_api_notes.py +++ b/tests/api/test_api_notes.py @@ -4,7 +4,8 @@ from ..fixtures.default_variables import TOKEN from ..fixtures.stubs import note_stub -from osm_easy_api import Api, Note +from osm_easy_api.api import Api +from osm_easy_api.data_classes import Note from osm_easy_api.api import exceptions as ApiExceptions def _are_notes_equal(first: Note, second: Note): diff --git a/tests/api/test_api_user.py b/tests/api/test_api_user.py index 369061b..bed3112 100644 --- a/tests/api/test_api_user.py +++ b/tests/api/test_api_user.py @@ -3,7 +3,7 @@ from ..fixtures.default_variables import TOKEN -from osm_easy_api import Api +from osm_easy_api.api import Api class TestApiElements(unittest.TestCase): diff --git a/tests/data_classes/test_node.py b/tests/data_classes/test_node.py index 8a70d23..69f0d62 100644 --- a/tests/data_classes/test_node.py +++ b/tests/data_classes/test_node.py @@ -1,6 +1,6 @@ import unittest -from osm_easy_api import Node, Tags +from osm_easy_api.data_classes import Node, Tags from ..fixtures import sample_dataclasses class TestNode(unittest.TestCase): diff --git a/tests/data_classes/test_osmChange.py b/tests/data_classes/test_osmChange.py index cc991aa..98c3642 100644 --- a/tests/data_classes/test_osmChange.py +++ b/tests/data_classes/test_osmChange.py @@ -1,7 +1,7 @@ import unittest import os -from osm_easy_api import Node, Way, OsmChange, Action, Tags, Relation +from osm_easy_api.data_classes import Node, Way, OsmChange, Action, Tags, Relation from ..fixtures import sample_dataclasses class TestOsmChange(unittest.TestCase): diff --git a/tests/data_classes/test_osmObjectPrimitive.py b/tests/data_classes/test_osmObjectPrimitive.py index 8d1861a..85f3b6e 100644 --- a/tests/data_classes/test_osmObjectPrimitive.py +++ b/tests/data_classes/test_osmObjectPrimitive.py @@ -1,6 +1,6 @@ import unittest -from osm_easy_api import Tags +from osm_easy_api.data_classes import Tags from osm_easy_api.data_classes.osm_object_primitive import osm_object_primitive class TestOsmObjectPrimitive(unittest.TestCase): diff --git a/tests/data_classes/test_relation.py b/tests/data_classes/test_relation.py index 01acba7..956ef3b 100644 --- a/tests/data_classes/test_relation.py +++ b/tests/data_classes/test_relation.py @@ -1,6 +1,6 @@ import unittest -from osm_easy_api import Relation, Way, Node, Tags +from osm_easy_api.data_classes import Relation, Way, Node, Tags from osm_easy_api.data_classes.relation import Member as RelationMember from ..fixtures import sample_dataclasses diff --git a/tests/data_classes/test_tags.py b/tests/data_classes/test_tags.py index 76b0d41..0e66c20 100644 --- a/tests/data_classes/test_tags.py +++ b/tests/data_classes/test_tags.py @@ -1,6 +1,6 @@ import unittest -from osm_easy_api import Tags +from osm_easy_api.data_classes import Tags from ..fixtures import sample_dataclasses class TestTags(unittest.TestCase): diff --git a/tests/data_classes/test_way.py b/tests/data_classes/test_way.py index 4021ea0..4c0e30c 100644 --- a/tests/data_classes/test_way.py +++ b/tests/data_classes/test_way.py @@ -1,6 +1,6 @@ import unittest -from osm_easy_api import Way, Tags +from osm_easy_api.data_classes import Way, Tags from ..fixtures import sample_dataclasses class TestWay(unittest.TestCase): diff --git a/tests/diff/test_diff_parser.py b/tests/diff/test_diff_parser.py index b5bfb65..8031a41 100644 --- a/tests/diff/test_diff_parser.py +++ b/tests/diff/test_diff_parser.py @@ -3,7 +3,7 @@ import os from osm_easy_api.diff.diff_parser import _OsmChange_parser -from osm_easy_api import Node, Way, Relation, Action, Tags +from osm_easy_api.data_classes import Node, Way, Relation, Action, Tags from osm_easy_api.data_classes.relation import Member class TestDiffParser(unittest.TestCase): diff --git a/tests/fixtures/sample_dataclasses.py b/tests/fixtures/sample_dataclasses.py index 03005a5..1c7475f 100644 --- a/tests/fixtures/sample_dataclasses.py +++ b/tests/fixtures/sample_dataclasses.py @@ -1,6 +1,6 @@ from copy import deepcopy -from osm_easy_api import Node, Way, Relation, Member, Tags, User, Comment, Note +from osm_easy_api.data_classes import Node, Way, Relation, Member, Tags, User, Comment, Note def node(key: str) -> Node: return deepcopy(_nodes[key]) diff --git a/tests/fixtures/stubs/note_stub.py b/tests/fixtures/stubs/note_stub.py index ac289d3..3f497d2 100644 --- a/tests/fixtures/stubs/note_stub.py +++ b/tests/fixtures/stubs/note_stub.py @@ -1,4 +1,4 @@ -from osm_easy_api import Note, Comment, User +from osm_easy_api.data_classes import Note, Comment, User XML_RESPONSE_BODY = """ From 91d82b9508e61d66a4b10bcc979989099bf1165c Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:16:14 +0100 Subject: [PATCH 30/50] Create test_gpxFile.py --- tests/data_classes/test_gpxFile.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/data_classes/test_gpxFile.py diff --git a/tests/data_classes/test_gpxFile.py b/tests/data_classes/test_gpxFile.py new file mode 100644 index 0000000..9e00d27 --- /dev/null +++ b/tests/data_classes/test_gpxFile.py @@ -0,0 +1,49 @@ +import unittest + +from osm_easy_api.data_classes import GpxFile, Visibility + +class TestWay(unittest.TestCase): + def test_basic_initalization(self): + gpxFile = GpxFile( + id=123, + name="hello.gpx", + user_id=111, + visibility=Visibility.PRIVATE, + pending=False, + timestamp="tomorrow", + latitude="10", + longitude="20", + description="hello world!", + tags=["alfa"] + ) + should_print = "GpxFile(id = 123, name = hello.gpx, user_id = 111, visibility = Visibility.PRIVATE, pending = False, timestamp = tomorrow, latitude = 10, longitude = 20, description = hello world!, tags = ['alfa'], )" + self.assertEqual(str(gpxFile), should_print) + + def test__to_xml(self): + gpxFile = GpxFile( + id=123, + name="hello.gpx", + user_id=111, + visibility=Visibility.PRIVATE, + pending=False, + timestamp="tomorrow", + latitude="10", + longitude="20", + description="hello world!", + tags=["alfa"] + ) + + element = gpxFile._to_xml() + self.assertEqual(element.tagName, "gpx_file") + self.assertEqual(element.getAttribute("id"), str(123)) + self.assertEqual(element.getAttribute("name"), "hello.gpx") + self.assertEqual(element.getAttribute("uid"), str(111)) + self.assertEqual(element.getAttribute("visibility"), "private") + self.assertEqual(element.getAttribute("pending"), "false") + self.assertEqual(element.getAttribute("timestamp"), "tomorrow") + self.assertEqual(element.getAttribute("lat"), "10") + self.assertEqual(element.getAttribute("lon"), "20") + + self.assertEqual(element.childNodes[0].tagName, "description") + self.assertEqual(element.childNodes[0].firstChild.nodeValue, "hello world!") + self.assertEqual(element.childNodes[1].firstChild.nodeValue, "alfa") \ No newline at end of file From 5f52af18f416155b476f60cd25bb99cd4f70c2d4 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:29:28 +0100 Subject: [PATCH 31/50] gpx.delete() --- CHANGELOG.md | 1 + src/osm_easy_api/api/_URLs.py | 3 ++- src/osm_easy_api/api/endpoints/gpx.py | 7 +++++-- src/osm_easy_api/diff/diff_parser.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1f99a..0d79521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `GpxFile` data class. - `Visibility` enum. - `gpx.update()` endpoint. +- `gpx.delete()` endpoint. ### Fixed - Types in `elements` endpoint. diff --git a/src/osm_easy_api/api/_URLs.py b/src/osm_easy_api/api/_URLs.py index 9f89b38..c20aa72 100644 --- a/src/osm_easy_api/api/_URLs.py +++ b/src/osm_easy_api/api/_URLs.py @@ -49,7 +49,8 @@ def __init__(self, base_url: str): self.gpx: Dict[str, str] = { "get": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}", "create": six_url + "/gpx/create", - "update": six_url + "/gpx/{id}" + "update": six_url + "/gpx/{id}", + "delete": six_url + "/gpx/{id}" } self.user: Dict[str, str] = { diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index d3ef037..0e98b03 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -40,7 +40,6 @@ def create(self, file_from: str, description: str, visibility: Visibility, tags: Returns: int: ID of the new trace. """ - # TODO: Enum instead of visibility string with open(file_from, "rb") as f: tags_string = None if tags: @@ -69,4 +68,8 @@ def update(self, gpx_file: GpxFile) -> None: xml.appendChild(gpx_file._to_xml()) xml_str = root.toprettyxml() - self.outer._request(method=self.outer._RequestMethods.PUT, url=self.outer._url.gpx["update"].format(id=gpx_file.id), body=xml_str) \ No newline at end of file + self.outer._request(method=self.outer._RequestMethods.PUT, url=self.outer._url.gpx["update"].format(id=gpx_file.id), body=xml_str) + + def delete(self, id: int) -> None: + self.outer._request(method=self.outer._RequestMethods.DELETE, url=self.outer._url.gpx["delete"].format(id=id)) + \ No newline at end of file diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index bed8ec0..d2745f6 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -57,7 +57,7 @@ def _is_correct(element: ElementTree.Element, tags: Tags | str) -> bool: raise ValueError("[ERROR::DIFF_PARSER::_IS_CORRECT] Unexpected return.") - +# TODO: Maybe move creation of object from xml to data classes? Node_Way_Relation = TypeVar("Node_Way_Relation", Node, Way, Relation) def _create_osm_object_from_attributes(element_type: Type[Node_Way_Relation], attributes: dict) -> Node_Way_Relation: From f0a91e6c79d072d6b547fe8c30f9de43a1b6d3ed Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:33:34 +0100 Subject: [PATCH 32/50] Update test_api_gpx.py --- tests/api/test_api_gpx.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index 3e0ac7c..dd2399b 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -70,4 +70,15 @@ def test_update(self): "status": 200, }) self.API.gpx.update(GPX_FILE) + self.assertTrue(responses.assert_call_count(URL, 1)) + + @responses.activate + def test_delete(self): + URL = "https://test.pl/api/0.6/gpx/123" + responses.add(**{ + "method": responses.DELETE, + "url": URL, + "status": 200, + }) + self.API.gpx.delete(123) self.assertTrue(responses.assert_call_count(URL, 1)) \ No newline at end of file From 1fe01279a77608e0b80296a0136a4af01f5ac3c1 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:33:43 +0100 Subject: [PATCH 33/50] Update API.txt --- API.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/API.txt b/API.txt index b738534..04cf6cc 100644 --- a/API.txt +++ b/API.txt @@ -38,8 +38,8 @@ ELEMENTS ✅ GPS TRACES ✅ GET /api/0.6/trackpoints?bbox=left,bottom,right,top&page=pageNumber - Get GPS tracks that are inside a given bbox ✅ POST /api/0.6/gpx/create - Create new GPS trace from GPX file -OO: PUT /api/0.6/gpx/#id - Update gps trace -OO: DELETE /api/0.6/gpx/#id - Delete gps trace +✅ OO: PUT /api/0.6/gpx/#id - Update gps trace +✅ OO: DELETE /api/0.6/gpx/#id - Delete gps trace GET /api/0.6/gpx/#id/details - Get osm data about GPS trace OO: if private GET /api/0.6/gpx/#id/data - Get GPX file for trace OO: if private OO: GET /api/0.6/user/gpx_files - Get all traces for owner account From 89839f70aeaf5fa2ed1eba7b2ebd3a80ce1e30f3 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:57:20 +0100 Subject: [PATCH 34/50] `gpx.get()` to `gpx.get_gps_points()` --- CHANGELOG.md | 3 ++- src/osm_easy_api/api/endpoints/gpx.py | 2 +- tests/api/test_api_gpx.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d79521..f1db828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OsmChange_parser_generator()` and `OsmChange_parser()` from `diff` module are now 'private' functions. Use `Diff.get()` instead. - `notes.search()` endpoint throws `LimitsExceeded` exception instead of `ValueError`. - `page_number` paremeter in `gpx.get()` has now default value 0. -- Now classes are imported from individual modules - not from the main library. See examples in the `README.md`. +- Now classes are imported from individual modules - not from the main library. See examples in the `README.md`. +- `gpx.get()` renamed to `gpx.get_gps_points()` ### Removed - Support for `HTTP Basic authentication`: `username` and `password` parameters in `Api` class constructor. diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index 0e98b03..8836a06 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -13,7 +13,7 @@ class Gpx_Container: def __init__(self, outer): self.outer: "Api" = outer - def get(self, file_to: str, left: str, bottom: str, right: str, top: str, page_number: int = 0) -> None: + def get_gps_points(self, file_to: str, left: str, bottom: str, right: str, top: str, page_number: int = 0) -> None: """Downloads gps points to file. Args: diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index dd2399b..d48b1bb 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -29,7 +29,7 @@ def test_get(self): "body": BODY, "status": 200 }) - self.API.gpx.get(F_TO_PATH, "10", "20", "30", "40", 1) + self.API.gpx.get_gps_points(F_TO_PATH, "10", "20", "30", "40", 1) self.assertTrue(responses.assert_call_count(URL, 1)) self.assertTrue(filecmp.cmp(F_FROM_PATH, F_TO_PATH, shallow=False)) os.remove(F_TO_PATH) From ad3cc2c12132f682f85356d2980d29b55213a5c4 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:27:17 +0100 Subject: [PATCH 35/50] gpx.get_details() --- CHANGELOG.md | 1 + src/osm_easy_api/api/_URLs.py | 3 +- src/osm_easy_api/api/endpoints/gpx.py | 57 ++++++++++++++++++++++++++- tests/api/test_api_gpx.py | 34 +++++++++++++++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1db828..3a2df62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Visibility` enum. - `gpx.update()` endpoint. - `gpx.delete()` endpoint. +- `gpx.get_details()` endpoint. ### Fixed - Types in `elements` endpoint. diff --git a/src/osm_easy_api/api/_URLs.py b/src/osm_easy_api/api/_URLs.py index c20aa72..c2e9fe7 100644 --- a/src/osm_easy_api/api/_URLs.py +++ b/src/osm_easy_api/api/_URLs.py @@ -50,7 +50,8 @@ def __init__(self, base_url: str): "get": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}", "create": six_url + "/gpx/create", "update": six_url + "/gpx/{id}", - "delete": six_url + "/gpx/{id}" + "delete": six_url + "/gpx/{id}", + "details": six_url + "/gpx/{id}/details" } self.user: Dict[str, str] = { diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index 8836a06..dcb4f4b 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -71,5 +71,60 @@ def update(self, gpx_file: GpxFile) -> None: self.outer._request(method=self.outer._RequestMethods.PUT, url=self.outer._url.gpx["update"].format(id=gpx_file.id), body=xml_str) def delete(self, id: int) -> None: + """Deletes a GPX file. + + Args: + id (int): ID of a GPX file to delete. + """ self.outer._request(method=self.outer._RequestMethods.DELETE, url=self.outer._url.gpx["delete"].format(id=id)) - \ No newline at end of file + + def get_details(self, id: int) -> GpxFile: + """Get details about trace. + + Args: + id (int): ID of a GPX file. + + Returns: + GpxFile: Requested GPX file details. + """ + string_to_visibility = { + "identifiable": Visibility.IDENTIFIABLE, + "public": Visibility.PUBLIC, + "trackable": Visibility.TRACKABLE, + "private": Visibility.PRIVATE + } + + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=self.outer._url.gpx["details"].format(id=id)) + + description = None + tags = [] + + for element in generator: + if element.tag == "description": description = element.text + elif element.tag == "tag": tags.append(element.text) + elif element.tag == "gpx_file": + id = int(element.get("id", -1)) + name = element.get("name") + user_id = int(element.get("uid", -1)) + visibility = string_to_visibility.get(element.get("visibility", "")) + pending = True if element.get("pending") == "true" else False + timestamp = element.get("timestamp") + latitude = element.get("lat") + longitude = element.get("lon") + + assert name and visibility and timestamp and latitude and longitude and description, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] missing members." + + return GpxFile( + id=id, + name=name, + user_id=user_id, + visibility=visibility, + pending=pending, + timestamp=timestamp, + latitude=latitude, + longitude=longitude, + description=description, + tags=tags + ) + + assert False, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] No GpxFile." \ No newline at end of file diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index d48b1bb..4a2c1ee 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -81,4 +81,36 @@ def test_delete(self): "status": 200, }) self.API.gpx.delete(123) - self.assertTrue(responses.assert_call_count(URL, 1)) \ No newline at end of file + self.assertTrue(responses.assert_call_count(URL, 1)) + + @responses.activate + def test_get_details(self): + URL = "https://test.pl/api/0.6/gpx/2418/details" + BODY = """ + + + HELLO WORLD + C + B + A + + +""" + responses.add(**{ + "method": responses.GET, + "url": URL, + "body": BODY, + "status": 200, + }) + gpxFile = self.API.gpx.get_details(2418) + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertEqual(gpxFile.id, 2418) + self.assertEqual(gpxFile.name, "aa.gpx") + self.assertEqual(gpxFile.user_id, 18179) + self.assertEqual(gpxFile.visibility, Visibility.TRACKABLE) + self.assertEqual(gpxFile.pending, False) + self.assertEqual(gpxFile.timestamp, "2024-03-17T18:48:06Z") + self.assertEqual(gpxFile.latitude, "52.238983") + self.assertEqual(gpxFile.longitude, "21.040647") + self.assertEqual(gpxFile.description, "HELLO WORLD") + self.assertEqual(gpxFile.tags, ["C", "B", "A"]) \ No newline at end of file From ae11b8a02afd699542dea696a54834ca59228244 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:19:19 +0100 Subject: [PATCH 36/50] gpx.get_file() --- API.txt | 4 ++-- CHANGELOG.md | 1 + src/osm_easy_api/api/_URLs.py | 5 +++-- src/osm_easy_api/api/endpoints/gpx.py | 15 +++++++++++++-- tests/api/test_api_gpx.py | 22 ++++++++++++++++++++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/API.txt b/API.txt index 04cf6cc..be2da66 100644 --- a/API.txt +++ b/API.txt @@ -40,8 +40,8 @@ GPS TRACES ✅ POST /api/0.6/gpx/create - Create new GPS trace from GPX file ✅ OO: PUT /api/0.6/gpx/#id - Update gps trace ✅ OO: DELETE /api/0.6/gpx/#id - Delete gps trace -GET /api/0.6/gpx/#id/details - Get osm data about GPS trace OO: if private -GET /api/0.6/gpx/#id/data - Get GPX file for trace OO: if private +✅ GET /api/0.6/gpx/#id/details - Get osm data about GPS trace OO: if private +✅ GET /api/0.6/gpx/#id/data - Get GPX file for trace OO: if private OO: GET /api/0.6/user/gpx_files - Get all traces for owner account USERS ✅ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a2df62..7887739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `gpx.update()` endpoint. - `gpx.delete()` endpoint. - `gpx.get_details()` endpoint. +- `gpx.get_file()` endpoint. ### Fixed - Types in `elements` endpoint. diff --git a/src/osm_easy_api/api/_URLs.py b/src/osm_easy_api/api/_URLs.py index c2e9fe7..9fa1357 100644 --- a/src/osm_easy_api/api/_URLs.py +++ b/src/osm_easy_api/api/_URLs.py @@ -47,11 +47,12 @@ def __init__(self, base_url: str): } self.gpx: Dict[str, str] = { - "get": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}", + "get_gps_points": six_url + "/trackpoints?bbox={left},{bottom},{right},{top}&page={page_number}", "create": six_url + "/gpx/create", "update": six_url + "/gpx/{id}", "delete": six_url + "/gpx/{id}", - "details": six_url + "/gpx/{id}/details" + "details": six_url + "/gpx/{id}/details", + "get_file": six_url + "/gpx/{id}/data" } self.user: Dict[str, str] = { diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index dcb4f4b..419029f 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -24,7 +24,7 @@ def get_gps_points(self, file_to: str, left: str, bottom: str, right: str, top: top (int): Bounding box page_number (int, optional): Which group of 5 000 points you want to get. Indexed from 0. Defaults to 0. """ - response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), stream=True) + response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get_gps_points"].format(left=left, bottom=bottom, right=right, top=top, page_number=page_number), stream=True) with open(file_to, "wb") as f_to: shutil.copyfileobj(response.raw, f_to) @@ -127,4 +127,15 @@ def get_details(self, id: int) -> GpxFile: tags=tags ) - assert False, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] No GpxFile." \ No newline at end of file + assert False, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] No GpxFile." + + def get_file(self, file_to: str, id: int) -> None: + """Downloads GPX file. + + Args: + file_to (str): Path where you want to save gpx. + id (int): ID of a GPX file to download. + """ + response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get_file"].format(id=id), stream=True) + with open(file_to, "wb") as f_to: + shutil.copyfileobj(response.raw, f_to) \ No newline at end of file diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index 4a2c1ee..268559a 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -17,7 +17,7 @@ def setUpClass(cls): cls.API = Api(url="https://test.pl", access_token=TOKEN) @responses.activate - def test_get(self): + def test_get_gps_points(self): URL = "https://test.pl/api/0.6/trackpoints?bbox=10,20,30,40&page=1" F_FROM_PATH = os.path.join("tests", "fixtures", "gps_points.gpx") F_TO_PATH = os.path.join("tests", "fixtures", "write_gps_points.gpx") @@ -113,4 +113,22 @@ def test_get_details(self): self.assertEqual(gpxFile.latitude, "52.238983") self.assertEqual(gpxFile.longitude, "21.040647") self.assertEqual(gpxFile.description, "HELLO WORLD") - self.assertEqual(gpxFile.tags, ["C", "B", "A"]) \ No newline at end of file + self.assertEqual(gpxFile.tags, ["C", "B", "A"]) + + @responses.activate + def test_get_file(self): + URL = "https://test.pl/api/0.6/gpx/2418/data" + F_FROM_PATH = os.path.join("tests", "fixtures", "gps_points.gpx") + F_TO_PATH = os.path.join("tests", "fixtures", "write_gps_points.gpx") + + with open(F_FROM_PATH, "rb") as BODY: + responses.add(**{ + "method": responses.GET, + "url": URL, + "body": BODY, + "status": 200 + }) + self.API.gpx.get_file(F_TO_PATH, 2418) + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertTrue(filecmp.cmp(F_FROM_PATH, F_TO_PATH, shallow=False)) + os.remove(F_TO_PATH) \ No newline at end of file From a5bbbc367a1fc2aea243bee560cd7127683284b9 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:59:24 +0100 Subject: [PATCH 37/50] gpx.list_details() --- API.txt | 2 +- CHANGELOG.md | 1 + src/osm_easy_api/api/_URLs.py | 3 +- src/osm_easy_api/api/endpoints/gpx.py | 98 +++++++++++++++------------ tests/api/test_api_gpx.py | 51 +++++++++++++- 5 files changed, 109 insertions(+), 46 deletions(-) diff --git a/API.txt b/API.txt index be2da66..3428648 100644 --- a/API.txt +++ b/API.txt @@ -42,7 +42,7 @@ GPS TRACES ✅ OO: DELETE /api/0.6/gpx/#id - Delete gps trace ✅ GET /api/0.6/gpx/#id/details - Get osm data about GPS trace OO: if private ✅ GET /api/0.6/gpx/#id/data - Get GPX file for trace OO: if private -OO: GET /api/0.6/user/gpx_files - Get all traces for owner account +✅ OO: GET /api/0.6/user/gpx_files - Get all traces for owner account USERS ✅ ✅ GET /api/0.6/user/#id - Get user data by id. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7887739..e978e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `gpx.delete()` endpoint. - `gpx.get_details()` endpoint. - `gpx.get_file()` endpoint. +- `gpx.list_details()` endpoint. ### Fixed - Types in `elements` endpoint. diff --git a/src/osm_easy_api/api/_URLs.py b/src/osm_easy_api/api/_URLs.py index 9fa1357..595b109 100644 --- a/src/osm_easy_api/api/_URLs.py +++ b/src/osm_easy_api/api/_URLs.py @@ -52,7 +52,8 @@ def __init__(self, base_url: str): "update": six_url + "/gpx/{id}", "delete": six_url + "/gpx/{id}", "details": six_url + "/gpx/{id}/details", - "get_file": six_url + "/gpx/{id}/data" + "get_file": six_url + "/gpx/{id}/data", + "list": six_url + "/user/gpx_files" } self.user: Dict[str, str] = { diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index 419029f..6341a71 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -1,13 +1,55 @@ import shutil -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generator from xml.dom import minidom +from xml.etree import ElementTree if TYPE_CHECKING: from ...api import Api from ...data_classes import GpxFile, Visibility -# TODO: GPX full support and parser + +def _xml_to_gpx_files(generator: Generator[ElementTree.Element, None, None]) -> list[GpxFile]: + string_to_visibility = { + "identifiable": Visibility.IDENTIFIABLE, + "public": Visibility.PUBLIC, + "trackable": Visibility.TRACKABLE, + "private": Visibility.PRIVATE + } + + gpx_files = [] + + description = None + tags = [] + for element in generator: + if element.tag == "description": description = element.text + elif element.tag == "tag": tags.append(element.text) + elif element.tag == "gpx_file": + id = int(element.get("id", -1)) + name = element.get("name") + user_id = int(element.get("uid", -1)) + visibility = string_to_visibility.get(element.get("visibility", "")) + pending = True if element.get("pending") == "true" else False + timestamp = element.get("timestamp") + latitude = element.get("lat") + longitude = element.get("lon") + assert name and visibility and timestamp and latitude and longitude and description, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] missing members." + gpx_files.append(GpxFile( + id=id, + name=name, + user_id=user_id, + visibility=visibility, + pending=pending, + timestamp=timestamp, + latitude=latitude, + longitude=longitude, + description=description, + tags=tags + )) + description = None + tags = [] + + return gpx_files class Gpx_Container: def __init__(self, outer): @@ -87,47 +129,8 @@ def get_details(self, id: int) -> GpxFile: Returns: GpxFile: Requested GPX file details. """ - string_to_visibility = { - "identifiable": Visibility.IDENTIFIABLE, - "public": Visibility.PUBLIC, - "trackable": Visibility.TRACKABLE, - "private": Visibility.PRIVATE - } - generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=self.outer._url.gpx["details"].format(id=id)) - - description = None - tags = [] - - for element in generator: - if element.tag == "description": description = element.text - elif element.tag == "tag": tags.append(element.text) - elif element.tag == "gpx_file": - id = int(element.get("id", -1)) - name = element.get("name") - user_id = int(element.get("uid", -1)) - visibility = string_to_visibility.get(element.get("visibility", "")) - pending = True if element.get("pending") == "true" else False - timestamp = element.get("timestamp") - latitude = element.get("lat") - longitude = element.get("lon") - - assert name and visibility and timestamp and latitude and longitude and description, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] missing members." - - return GpxFile( - id=id, - name=name, - user_id=user_id, - visibility=visibility, - pending=pending, - timestamp=timestamp, - latitude=latitude, - longitude=longitude, - description=description, - tags=tags - ) - - assert False, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] No GpxFile." + return _xml_to_gpx_files(generator)[0] def get_file(self, file_to: str, id: int) -> None: """Downloads GPX file. @@ -138,4 +141,13 @@ def get_file(self, file_to: str, id: int) -> None: """ response = self.outer._request(self.outer._RequestMethods.GET, self.outer._url.gpx["get_file"].format(id=id), stream=True) with open(file_to, "wb") as f_to: - shutil.copyfileobj(response.raw, f_to) \ No newline at end of file + shutil.copyfileobj(response.raw, f_to) + + def list_details(self) -> list[GpxFile]: + """Get list of GPX traces owned by current authenticated user. + + Returns: + list[GpxFile]: List of gpx files details. + """ + generator = self.outer._request_generator(method=self.outer._RequestMethods.GET, url=self.outer._url.gpx["list"]) + return _xml_to_gpx_files(generator) \ No newline at end of file diff --git a/tests/api/test_api_gpx.py b/tests/api/test_api_gpx.py index 268559a..c33852e 100644 --- a/tests/api/test_api_gpx.py +++ b/tests/api/test_api_gpx.py @@ -131,4 +131,53 @@ def test_get_file(self): self.API.gpx.get_file(F_TO_PATH, 2418) self.assertTrue(responses.assert_call_count(URL, 1)) self.assertTrue(filecmp.cmp(F_FROM_PATH, F_TO_PATH, shallow=False)) - os.remove(F_TO_PATH) \ No newline at end of file + os.remove(F_TO_PATH) + + @responses.activate + def test_list_details(self): + URL = "https://test.pl/api/0.6/user/gpx_files" + BODY = """ + + + HELLO WORLD + C + B + A + + + ęśąćź#$%!#@$%ęśąćź + ęśąćź!@$*() + ęśąćź!@ + + +""" + responses.add(**{ + "method": responses.GET, + "url": URL, + "body": BODY, + "status": 200, + }) + gpxFiles = self.API.gpx.list_details() + self.assertTrue(responses.assert_call_count(URL, 1)) + self.assertEqual(gpxFiles[0].id, 2418) + self.assertEqual(gpxFiles[0].name, "aa.gpx") + self.assertEqual(gpxFiles[0].user_id, 18179) + self.assertEqual(gpxFiles[0].visibility, Visibility.TRACKABLE) + self.assertEqual(gpxFiles[0].pending, False) + self.assertEqual(gpxFiles[0].timestamp, "2024-03-17T18:48:06Z") + self.assertEqual(gpxFiles[0].latitude, "52.238983") + self.assertEqual(gpxFiles[0].longitude, "21.040647") + self.assertEqual(gpxFiles[0].description, "HELLO WORLD") + self.assertEqual(gpxFiles[0].tags, ["C", "B", "A"]) + + self.assertEqual(gpxFiles[1].id, 2417) + self.assertEqual(gpxFiles[1].name, "aa.gpx") + self.assertEqual(gpxFiles[1].user_id, 18179) + self.assertEqual(gpxFiles[1].visibility, Visibility.TRACKABLE) + self.assertEqual(gpxFiles[1].pending, False) + self.assertEqual(gpxFiles[1].timestamp, "2024-03-17T18:44:07Z") + self.assertEqual(gpxFiles[1].latitude, "52.238983") + self.assertEqual(gpxFiles[1].longitude, "21.040647") + self.assertEqual(gpxFiles[1].description, "ęśąćź#$%!#@$%ęśąćź") + # ęśąćź!@$^&*() + self.assertEqual(gpxFiles[1].tags, ["ęśąćź!@$*()", "ęśąćź!@"]) \ No newline at end of file From 0f6174be5602db4e09848d51ce1454f4aea221d0 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Mon, 18 Mar 2024 20:11:41 +0100 Subject: [PATCH 38/50] _from_xml() in data_classes --- src/osm_easy_api/api/api.py | 2 +- src/osm_easy_api/api/endpoints/elements.py | 20 +++--- src/osm_easy_api/data_classes/node.py | 13 +++- .../data_classes/osm_object_primitive.py | 21 ++++++ src/osm_easy_api/data_classes/relation.py | 20 +++++- src/osm_easy_api/data_classes/way.py | 11 ++++ src/osm_easy_api/diff/diff_parser.py | 64 +------------------ src/osm_easy_api/utils/__init__.py | 3 +- .../utils/element_to_osm_object.py | 15 +++++ 9 files changed, 94 insertions(+), 75 deletions(-) create mode 100644 src/osm_easy_api/utils/element_to_osm_object.py diff --git a/src/osm_easy_api/api/api.py b/src/osm_easy_api/api/api.py index 37bb842..ec0a806 100644 --- a/src/osm_easy_api/api/api.py +++ b/src/osm_easy_api/api/api.py @@ -2,7 +2,7 @@ from xml.etree import ElementTree from enum import Enum -from typing import TYPE_CHECKING, Generator, Tuple +from typing import TYPE_CHECKING, Generator if TYPE_CHECKING: from urllib3.response import HTTPResponse from requests.models import Response diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index 8e590c8..cfb94f8 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -7,7 +7,7 @@ from ...api import exceptions from ...data_classes import Node, Way, Relation from ...data_classes.relation import Member -from ...diff.diff_parser import _element_to_osm_object +from ...utils import element_to_osm_object Node_Way_Relation = TypeVar("Node_Way_Relation", Node, Way, Relation) Way_Relation = TypeVar("Way_Relation", Way, Relation) @@ -59,7 +59,7 @@ def get(self, element_type: Type[Node_Way_Relation], id: int) -> Node_Way_Relati for elem in generator: if elem.tag in ("node", "way", "relation"): - object = _element_to_osm_object(elem) + object = element_to_osm_object(elem) return cast(element_type, object) assert False, "No objects to parse!" @@ -127,7 +127,7 @@ def history(self, element_type: Type[Node_Way_Relation], id: int) -> list[Node_W objects_list = [] for elem in generator: if elem.tag == element_name: - objects_list.append(_element_to_osm_object(elem)) + objects_list.append(element_to_osm_object(elem)) return objects_list @@ -156,7 +156,7 @@ def version(self, element_type: Type[Node_Way_Relation], id: int, version: int) for elem in generator: if elem.tag in ("node", "way", "relation"): - return cast(Node_Way_Relation, _element_to_osm_object(elem)) + return cast(Node_Way_Relation, element_to_osm_object(elem)) assert False, "[ERROR::API::ENDPOINTS::ELEMENTS::version] Cannot create an element." def get_query(self, element_type: Type[Node_Way_Relation], ids: list[int]) -> list[Node_Way_Relation]: @@ -187,7 +187,7 @@ def get_query(self, element_type: Type[Node_Way_Relation], ids: list[int]) -> li objects_list = [] for elem in generator: if elem.tag == element_type.__name__.lower(): - objects_list.append(_element_to_osm_object(elem)) + objects_list.append(element_to_osm_object(elem)) return objects_list @@ -208,7 +208,7 @@ def relations(self, element_type: Type[Node | Way | Relation], id: int) -> list[ relations_list = [] for elem in generator: if elem.tag == "relation": - relations_list.append(_element_to_osm_object(elem)) + relations_list.append(element_to_osm_object(elem)) return relations_list @@ -227,7 +227,7 @@ def ways(self, node_id: int) -> list[Way]: ways_list = [] for elem in generator: if elem.tag == "way": - ways_list.append(_element_to_osm_object(elem)) + ways_list.append(element_to_osm_object(elem)) return ways_list @@ -250,15 +250,15 @@ def full(self, element_type: Type[Way_Relation], id: int) -> Way_Relation: relations_dict: dict[int, Relation] = {} for elem in generator: if elem.tag == "node": - node = cast(Node, _element_to_osm_object(elem)) + node = cast(Node, element_to_osm_object(elem)) assert node.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" nodes_dict.update({node.id: node}) if elem.tag == "way": - way = cast(Way, _element_to_osm_object(elem)) + way = cast(Way, element_to_osm_object(elem)) assert way.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" ways_dict.update({way.id: way}) if elem.tag == "relation" and element_name == "relation": - relation = cast(Relation, _element_to_osm_object(elem)) + relation = cast(Relation, element_to_osm_object(elem)) assert relation.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" relations_dict.update({relation.id: relation}) diff --git a/src/osm_easy_api/data_classes/node.py b/src/osm_easy_api/data_classes/node.py index 52f9f10..379be00 100644 --- a/src/osm_easy_api/data_classes/node.py +++ b/src/osm_easy_api/data_classes/node.py @@ -1,6 +1,10 @@ from dataclasses import dataclass from xml.dom import minidom +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from xml.etree import ElementTree + from ..data_classes.osm_object_primitive import osm_object_primitive @dataclass @@ -26,4 +30,11 @@ def _to_xml(self, changeset_id, way_version = False, member_version = False, rol for tag in self.tags._to_xml(): element.appendChild(tag) - return element \ No newline at end of file + return element + + @classmethod + def _from_xml(cls, element: 'ElementTree.Element'): + node: Node = super()._from_xml(element) + node.latitude = str(element.attrib.get("lat")) + node.longitude = str(element.attrib.get("lon")) + return node \ No newline at end of file diff --git a/src/osm_easy_api/data_classes/osm_object_primitive.py b/src/osm_easy_api/data_classes/osm_object_primitive.py index 84d87e8..e1f6dfe 100644 --- a/src/osm_easy_api/data_classes/osm_object_primitive.py +++ b/src/osm_easy_api/data_classes/osm_object_primitive.py @@ -2,6 +2,10 @@ from xml.dom import minidom from copy import copy +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from xml.etree.ElementTree import Element + from ..data_classes.tags import Tags @dataclass @@ -36,6 +40,23 @@ def _to_xml(self, changeset_id, member_version=False, role="") -> minidom.Elemen element.setAttribute("version", str(self.version)) element.setAttribute("changeset", str(changeset_id)) return element + + @classmethod + def _from_xml(cls, element: 'Element'): + attrib = element.attrib + id = int(attrib["id"]) + visible = None + if attrib.get("visible"): + visible = True if attrib["visible"] == "true" else False + version = int(attrib["version"]) + timestamp = str(attrib["timestamp"]) + user_id = int(attrib.get("uid", -1)) + changeset_id = int(attrib["changeset"]) + obj = cls(id=id, visible=visible, version=version, timestamp=timestamp, user_id=user_id, changeset_id=changeset_id) + for tag in element: + if tag.tag == "tag": obj.tags.add(tag.attrib["k"], tag.attrib["v"]) + + return obj def to_dict(self) -> dict[str, str]: """Returns a dictionary that corresponds to the attributes of the object. In addition, a 'type' key is added to specify the type of element. diff --git a/src/osm_easy_api/data_classes/relation.py b/src/osm_easy_api/data_classes/relation.py index 7e08889..48402d6 100644 --- a/src/osm_easy_api/data_classes/relation.py +++ b/src/osm_easy_api/data_classes/relation.py @@ -1,6 +1,8 @@ from dataclasses import dataclass, field -from typing import NamedTuple from copy import copy +from typing import NamedTuple, TYPE_CHECKING +if TYPE_CHECKING: + from xml.etree.ElementTree import Element from ..data_classes.osm_object_primitive import osm_object_primitive from ..data_classes import Node, Way @@ -29,6 +31,22 @@ def _to_xml(self, changeset_id, member_version=False, role=""): return element + @classmethod + def _from_xml(cls, element: 'Element'): + relation: Relation = super()._from_xml(element) + + def _append_member(type: type[Node | Way | Relation], member_attrib: dict) -> None: + relation.members.append(Member(type(id=int(member_attrib["ref"])), member_attrib["role"])) + + for member in element: + if member.tag == "member": + match member.attrib["type"]: + case "node": _append_member(Node, member.attrib) + case "way": _append_member(Way, member.attrib) + case "relation": _append_member(Relation, member.attrib) + + return relation + def to_dict(self) -> dict[str, str | list[_MEMBER_DICTIONARY_TYPE]]: super_dict: dict[str, str | list[_MEMBER_DICTIONARY_TYPE]] = super().to_dict() # type: ignore members: list[_MEMBER_DICTIONARY_TYPE] = [] diff --git a/src/osm_easy_api/data_classes/way.py b/src/osm_easy_api/data_classes/way.py index 0c7b8d9..f0fa76c 100644 --- a/src/osm_easy_api/data_classes/way.py +++ b/src/osm_easy_api/data_classes/way.py @@ -1,6 +1,10 @@ from dataclasses import dataclass, field from copy import copy +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from xml.etree.ElementTree import Element + from ..data_classes.osm_object_primitive import osm_object_primitive from ..data_classes.node import Node @@ -24,6 +28,13 @@ def _to_xml(self, changeset_id, member_version=False, role=""): element.appendChild(node_element) return element + @classmethod + def _from_xml(cls, element: 'Element'): + way: Way = super()._from_xml(element) + for nd in element: + if nd.tag == "nd": way.nodes.append(Node(id=int(nd.attrib["ref"]))) + return way + def to_dict(self) -> dict[str, str | list[dict[str, str]]]: super_dict: dict[str, str | list[dict[str, str]]] = super().to_dict() # type: ignore nodes: list[dict[str, str]] = [] diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index d2745f6..b198188 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -1,11 +1,11 @@ from xml.etree import ElementTree -from typing import Generator, cast, TYPE_CHECKING, TypeVar, Type +from typing import Generator, cast, TYPE_CHECKING if TYPE_CHECKING: import gzip from ..data_classes import Node, Way, Relation, OsmChange, Action, Tags -from ..data_classes.relation import Member from ..data_classes.OsmChange import Meta +from ..utils import element_to_osm_object STRING_TO_ACTION = { "create": Action.CREATE, @@ -13,23 +13,6 @@ "delete": Action.DELETE } -def _add_members_to_relation_from_element(relation: Relation, element: ElementTree.Element) -> None: - def _append_member(relation: Relation, type: type[Node | Way | Relation], member_attrib: dict) -> None: - relation.members.append(Member(type(id=int(member_attrib["ref"])), member_attrib["role"])) - - for member in element: - if member.tag == "member": - match member.attrib["type"]: - case "node": _append_member(relation, Node, member.attrib) - case "way": _append_member(relation, Way, member.attrib) - case "relation": _append_member(relation, Relation, member.attrib) - -def _add_nodes_to_way_from_element(way: Way, element: ElementTree.Element) -> None: - for nd in element: - if nd.tag == "nd": - way.nodes.append(Node(id=int(nd.attrib["ref"]))) - - def _is_correct(element: ElementTree.Element, tags: Tags | str) -> bool: """Checks if provided element has all required tags. @@ -57,47 +40,6 @@ def _is_correct(element: ElementTree.Element, tags: Tags | str) -> bool: raise ValueError("[ERROR::DIFF_PARSER::_IS_CORRECT] Unexpected return.") -# TODO: Maybe move creation of object from xml to data classes? -Node_Way_Relation = TypeVar("Node_Way_Relation", Node, Way, Relation) -def _create_osm_object_from_attributes(element_type: Type[Node_Way_Relation], attributes: dict) -> Node_Way_Relation: - - id = int(attributes["id"]) - visible = None - if attributes.get("visible"): - visible = True if attributes["visible"] == "true" else False - version = int(attributes["version"]) - timestamp = str(attributes["timestamp"]) - user_id = int(attributes.get("uid", -1)) - changeset_id = int(attributes["changeset"]) - - element = element_type(id=id, visible=visible, version=version, timestamp=timestamp, user_id=user_id, changeset_id=changeset_id) - - if type(element) == Node: - element.latitude = str(attributes.get("lat")) - element.longitude = str(attributes.get("lon")) - - return element - -def _element_to_osm_object(element: ElementTree.Element) -> Node | Way | Relation: - def append_tags(element: ElementTree.Element, append_to: Node | Way | Relation): - for tag in element: - if tag.tag == "tag": append_to.tags.add(tag.attrib["k"], tag.attrib["v"]) - - osmObject = None - match element.tag: - case "node": - osmObject = _create_osm_object_from_attributes(Node, element.attrib) - case "way": - osmObject = _create_osm_object_from_attributes(Way, element.attrib) - _add_nodes_to_way_from_element(osmObject, element) - case "relation": - osmObject = _create_osm_object_from_attributes(Relation, element.attrib) - _add_members_to_relation_from_element(osmObject, element) - case _: assert False, f"[ERROR::DIFF_PARSER::_ELEMENT_TO_OSM_OBJECT] Unknown element tag: {element.tag}" - - append_tags(element, osmObject) - return osmObject - def _OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: """Generator with elements in diff file. First yield will be Meta namedtuple. @@ -120,7 +62,7 @@ def _OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | No if element.tag in ("modify", "create", "delete"): action = STRING_TO_ACTION.get(element.tag, Action.NONE) elif element.tag in ("node", "way", "relation") and _is_correct(element, required_tags): - osmObject = _element_to_osm_object(element) + osmObject = element_to_osm_object(element) yield(action, osmObject) element.clear() diff --git a/src/osm_easy_api/utils/__init__.py b/src/osm_easy_api/utils/__init__.py index 33b1a2e..81e7a9a 100644 --- a/src/osm_easy_api/utils/__init__.py +++ b/src/osm_easy_api/utils/__init__.py @@ -1,2 +1,3 @@ from .join_url import join_url -from .write_gzip_to_file import write_gzip_to_file \ No newline at end of file +from .write_gzip_to_file import write_gzip_to_file +from .element_to_osm_object import element_to_osm_object \ No newline at end of file diff --git a/src/osm_easy_api/utils/element_to_osm_object.py b/src/osm_easy_api/utils/element_to_osm_object.py new file mode 100644 index 0000000..297ed1a --- /dev/null +++ b/src/osm_easy_api/utils/element_to_osm_object.py @@ -0,0 +1,15 @@ +from osm_easy_api.data_classes import Node, Way, Relation + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from xml.etree.ElementTree import Element + +def element_to_osm_object(element: 'Element') -> Node | Way | Relation: + match element.tag: + case "node": + return Node._from_xml(element) + case "way": + return Way._from_xml(element) + case "relation": + return Relation._from_xml(element) + case _: assert False, f"[ERROR::DIFF_PARSER::_ELEMENT_TO_OSM_OBJECT] Unknown element tag: {element.tag}" \ No newline at end of file From 37dfc22558c0736ce5d2fe2f67a126755e18c41d Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:13:45 +0100 Subject: [PATCH 39/50] Update test_diff.py --- tests/diff/test_diff.py | 192 +++++++++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 69 deletions(-) diff --git a/tests/diff/test_diff.py b/tests/diff/test_diff.py index 07185a7..8fd3dbe 100644 --- a/tests/diff/test_diff.py +++ b/tests/diff/test_diff.py @@ -2,83 +2,104 @@ import responses import os -import osm_easy_api.diff as diff +from osm_easy_api.diff import Diff, Frequency +from osm_easy_api.data_classes import OsmChange, Node, Way, Relation, Action class TestDiff(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.SEQUENCE_NUMBER = "5315422" + cls.SEQUENCE_NUMBER_NEW_LINE = "5315422\n" + @responses.activate def test_diff__get_state(self): - body = "#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber=5315422\ntimestamp=2022-11-12T14\\:22\\:07Z" - responses.add(**{ - "method": responses.GET, - "url": "https://test.pl/minute/state.txt", - "body": body, - "status": 200 - }) + URL_MINUTE = "https://test.pl/minute/state.txt" + URL_HOUR = "https://test.pl/hour/state.txt" + URL_DAY = "https://test.pl/day/state.txt" + BODY = "#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber=5315422\ntimestamp=2022-11-12T14\\:22\\:07Z" + DIFF = Diff(Frequency.MINUTE, "https://test.pl") - responses.add(**{ - "method": responses.GET, - "url": "https://test.pl/hour/state.txt", - "body": body, - "status": 404 - }) + def addResponse(url: str, body: str, status_code: int): + responses.add(**{ + "method": responses.GET, + "url": url, + "body": body, + "status": status_code + }) - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - self.assertEqual(d._get_state(), body) - - d = diff.Diff(diff.Frequency.HOUR, "https://test.pl") - self.assertRaises(ValueError, d._get_state) + # 200 + addResponse(URL_MINUTE, BODY, 200) + DIFF.frequency = Frequency.MINUTE + self.assertEqual(DIFF._get_state(), BODY) + self.assertTrue(responses.assert_call_count(URL_MINUTE, 1)) + + addResponse(URL_HOUR, BODY, 200) + DIFF.frequency = Frequency.HOUR + self.assertEqual(DIFF._get_state(), BODY) + self.assertTrue(responses.assert_call_count(URL_HOUR, 1)) + + addResponse(URL_DAY, BODY, 200) + DIFF.frequency = Frequency.DAY + self.assertEqual(DIFF._get_state(), BODY) + self.assertTrue(responses.assert_call_count(URL_DAY, 1)) + + # 404 ValueError + addResponse(URL_MINUTE, BODY, 404) + DIFF.frequency = Frequency.MINUTE + self.assertRaises(ValueError, DIFF._get_state) + self.assertTrue(responses.assert_call_count(URL_MINUTE, 2)) + + addResponse(URL_HOUR, BODY, 404) + DIFF.frequency = Frequency.HOUR + self.assertRaises(ValueError, DIFF._get_state) + self.assertTrue(responses.assert_call_count(URL_HOUR, 2)) + + addResponse(URL_DAY, BODY, 404) + DIFF.frequency = Frequency.DAY + self.assertRaises(ValueError, DIFF._get_state) + self.assertTrue(responses.assert_call_count(URL_DAY, 2)) def test_diff__get_sequence_number_from_state(self): - # With new line - sequence_number = "5315422\n" - sequence_number_expected = "5315422" - body = f"#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber={sequence_number}timestamp=2022-11-12T14\\:22\\:07Z" - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - self.assertEqual(d._get_sequence_number_from_state(body), sequence_number_expected) - - # Without new line - sequence_number = "5315422" - sequence_number_expected = "5315422" - body = f"#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber={sequence_number}timestamp=2022-11-12T14\\:22\\:07Z" - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - self.assertEqual(d._get_sequence_number_from_state(body), sequence_number_expected) + BODY = "#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber={sequence_number}timestamp=2022-11-12T14\\:22\\:07Z" + DIFF = Diff(Frequency.MINUTE, "https://test.pl") + + self.assertEqual(DIFF._get_sequence_number_from_state(BODY.format(sequence_number=self.SEQUENCE_NUMBER)), self.SEQUENCE_NUMBER) + self.assertEqual(DIFF._get_sequence_number_from_state(BODY.format(sequence_number=self.SEQUENCE_NUMBER_NEW_LINE)), self.SEQUENCE_NUMBER) @responses.activate def test_diff_get_sequence_number(self): - sequence_number = "5315422\n" - sequence_number_expected = "5315422" - body = f"#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber={sequence_number}timestamp=2022-11-12T14\\:22\\:07Z" + BODY = f"#Sat Nov 12 14:22:10 UTC 2022\nsequenceNumber={self.SEQUENCE_NUMBER_NEW_LINE}timestamp=2022-11-12T14\\:22\\:07Z" + DIFF = Diff(Frequency.MINUTE, "https://test.pl") + responses.add(**{ "method": responses.GET, "url": "https://test.pl/minute/state.txt", - "body": body, + "body": BODY, "status": 200 }) - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - self.assertEqual(d.get_sequence_number(), sequence_number_expected) + self.assertEqual(DIFF.get_sequence_number(), self.SEQUENCE_NUMBER) def test_diff__build_url(self): - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - self.assertEqual(d._build_url(d.url, d.frequency, "5"), "https://test.pl/minute/000/000/005.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "53"), "https://test.pl/minute/000/000/053.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "531"), "https://test.pl/minute/000/000/531.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "5315"), "https://test.pl/minute/000/005/315.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "53154"), "https://test.pl/minute/000/053/154.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "531542"), "https://test.pl/minute/000/531/542.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "5315422"), "https://test.pl/minute/005/315/422.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "53154221"), "https://test.pl/minute/053/154/221.osc.gz") - self.assertEqual(d._build_url(d.url, d.frequency, "531542219"), "https://test.pl/minute/531/542/219.osc.gz") + DIFF = Diff(Frequency.MINUTE, "https://test.pl") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "5"), "https://test.pl/minute/000/000/005.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "53"), "https://test.pl/minute/000/000/053.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "531"), "https://test.pl/minute/000/000/531.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "5315"), "https://test.pl/minute/000/005/315.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "53154"), "https://test.pl/minute/000/053/154.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "531542"), "https://test.pl/minute/000/531/542.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "5315422"), "https://test.pl/minute/005/315/422.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "53154221"), "https://test.pl/minute/053/154/221.osc.gz") + self.assertEqual(DIFF._build_url(DIFF.url, DIFF.frequency, "531542219"), "https://test.pl/minute/531/542/219.osc.gz") @responses.activate def test_diff_get_generator(self): - sequence_number = "5315422" body = open(os.path.join("tests", "fixtures", "hour.xml.gz"), "rb") responses.add(**{ "method": responses.GET, "url": "https://test.pl/minute/state.txt", - "body": sequence_number, + "body": self.SEQUENCE_NUMBER, "status": 200 }) @@ -89,32 +110,65 @@ def test_diff_get_generator(self): "status": 200 }) - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - meta, gen = d.get(sequence_number=sequence_number) + d = Diff(Frequency.MINUTE, "https://test.pl") + meta, gen = d.get(sequence_number=self.SEQUENCE_NUMBER) act, element = next(gen) - self.assertEqual(meta.sequence_number, sequence_number) + self.assertEqual(meta.sequence_number, self.SEQUENCE_NUMBER) self.assertEqual(element.id, 4222078) @responses.activate def test_diff_get(self): - sequence_number = "5315422" - body = open(os.path.join("tests", "fixtures", "hour.xml.gz"), "rb") - responses.add(**{ + def check_osm_change(osm_change: OsmChange): + self.assertEqual(osm_change.get(Node, Action.CREATE).__len__(), 2) + self.assertEqual(osm_change.get(Node, Action.MODIFY).__len__(), 14) + self.assertEqual(osm_change.get(Node, Action.DELETE).__len__(), 1) + self.assertEqual(osm_change.get(Node, Action.NONE).__len__(), 0) + + self.assertEqual(osm_change.get(Way, Action.CREATE).__len__(), 0) + self.assertEqual(osm_change.get(Way, Action.MODIFY).__len__(), 1) + self.assertEqual(osm_change.get(Way, Action.DELETE).__len__(), 0) + self.assertEqual(osm_change.get(Way, Action.NONE).__len__(), 0) + + self.assertEqual(osm_change.get(Relation, Action.CREATE).__len__(), 1) + self.assertEqual(osm_change.get(Relation, Action.MODIFY).__len__(), 0) + self.assertEqual(osm_change.get(Relation, Action.DELETE).__len__(), 0) + self.assertEqual(osm_change.get(Relation, Action.NONE).__len__(), 0) + + with open(os.path.join("tests", "fixtures", "hour.xml.gz"), "rb") as f: + responses.add(**{ + "method": responses.GET, + "url": "https://test.pl/minute/005/315/422.osc.gz", + "body": f, + "status": 200 + }) + + DIFF = Diff(Frequency.MINUTE, "https://test.pl") + osm_change = DIFF.get(sequence_number=self.SEQUENCE_NUMBER, generator=False) + assert isinstance(osm_change, OsmChange) + check_osm_change(osm_change) + + self.assertTrue(responses.assert_call_count("https://test.pl/minute/state.txt", 0)) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/005/315/422.osc.gz", 1)) + + with open(os.path.join("tests", "fixtures", "hour.xml.gz"), "rb") as f: + responses.add(**{ + "method": responses.GET, + "url": "https://test.pl/minute/005/315/422.osc.gz", + "body": f, + "status": 200 + }) + + responses.add(**{ "method": responses.GET, "url": "https://test.pl/minute/state.txt", - "body": sequence_number, + "body": f"""#Mon Mar 18 19:34:09 UTC 2024 +sequenceNumber={self.SEQUENCE_NUMBER} +timestamp=2024-03-18T19\:33\:45Z""", "status": 200 }) - - responses.add(**{ - "method": responses.GET, - "url": "https://test.pl/minute/005/315/422.osc.gz", - "body": body, - "status": 200 - }) - - d = diff.Diff(diff.Frequency.MINUTE, "https://test.pl") - osmChange = d.get(sequence_number=sequence_number, generator=False) - should_print = f"OsmChange(version=0.6, generator=Osmosis 0.47.4, sequence_number={sequence_number}. Node: Create(2), Modify(14), Delete(1), None(0). Way: Create(0), Modify(1), Delete(0), None(0). Relation: Create(1), Modify(0), Delete(0), None(0)." - self.assertEqual(str(osmChange), should_print) \ No newline at end of file + osm_change = DIFF.get(generator=False) + assert isinstance(osm_change, OsmChange) + check_osm_change(osm_change) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/state.txt", 1)) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/005/315/422.osc.gz", 2)) From 55e3642bd5bfbd2fdfe46603db176bc6d8dd6e8a Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:24:50 +0100 Subject: [PATCH 40/50] Update diff.py --- src/osm_easy_api/diff/diff.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/osm_easy_api/diff/diff.py b/src/osm_easy_api/diff/diff.py index 81f5b16..8a8d944 100644 --- a/src/osm_easy_api/diff/diff.py +++ b/src/osm_easy_api/diff/diff.py @@ -24,12 +24,17 @@ def frequency_to_str(frequency: Frequency) -> str: class Diff(): def __init__(self, frequency: Frequency, url: str = "https://planet.openstreetmap.org/replication", standard_url_frequency_format: bool = True, user_agent: str | None = None): + """ + Args: + frequency (Frequency): Time granularity. + url (_type_, optional): Replication server url. Defaults to "https://planet.openstreetmap.org/replication". + standard_url_frequency_format (bool, optional): If url to the state.txt file should contain time granularity. Defaults to True. + user_agent (str | None, optional): User agent used during requests. Defaults to None. + """ self.url = url self.frequency = frequency self.standard_url_frequency_format = standard_url_frequency_format - - if user_agent: - self._user_agent = user_agent + self._headers = {"User-Agent": user_agent} if user_agent else {} @staticmethod def _get_sequence_number_from_state(state_txt: str) -> str: @@ -55,10 +60,7 @@ def _get_state(self) -> str: """Downloads state.txt file content from diff server.""" if self.standard_url_frequency_format: url = join_url(self.url, frequency_to_str(self.frequency), "state.txt") else: url = join_url(self.url, "state.txt") - headers = {} - if hasattr(self, "_user_agent"): - headers.update({"User-Agent": self._user_agent}) - response = requests.get(url, headers=headers) + response = requests.get(url, headers=self._headers) if response.status_code != 200: raise ValueError(f"[ERROR::DIFF::_GET_STATE] API RESPONSE STATUS CODE: {response.status_code}") return response.text @@ -119,10 +121,8 @@ def get(self, sequence_number: str | None = None, file_to: str | None = None, fi if self.standard_url_frequency_format: url = self._build_url(self.url, self.frequency, sequence_number) else: url = self._build_url(self.url, None, sequence_number) - headers = {} - if hasattr(self, "_user_agent"): - headers.update({"User-Agent": self._user_agent}) - response = requests.get(url, stream=True, headers=headers) + + response = requests.get(url, stream=True, headers=self._headers) file = gzip.GzipFile(fileobj=response.raw) From b40dcf3082a253a5ada6e0e0acea0261dbb934ee Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:33:10 +0100 Subject: [PATCH 41/50] Update test_note.py --- tests/data_classes/test_note.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/data_classes/test_note.py b/tests/data_classes/test_note.py index 80df09e..5041f2c 100644 --- a/tests/data_classes/test_note.py +++ b/tests/data_classes/test_note.py @@ -2,6 +2,7 @@ from osm_easy_api.data_classes import Note, Comment, User from ..fixtures import sample_dataclasses +from ..fixtures.stubs import note_stub class TestComment(unittest.TestCase): def test_basic_initalization(self): @@ -14,6 +15,12 @@ def test_basic_initalization(self): self.assertEqual(comment.text, "ABC") self.assertEqual(comment.html, "ABC") + def test__str__(self): + comment = sample_dataclasses.comment("simple_1") + + should_print = """Comment(comment_created_at = 123, user = User(id = 123, display_name = abc, account_created_at = None, description = None, contributor_terms_agreed = None, img_url = None, roles = None, changesets_count = None, traces_count = None, blocks = None, ), action = opened, text = ABC, html = ABC, )""" + self.assertEqual(comment.__str__(), should_print) + def test_to_from_dict(self): comment = sample_dataclasses.comment("full_1") @@ -48,11 +55,10 @@ def test_basic_initalization(self): self.assertEqual(note.open, True) self.assertEqual(note.comments[0], comment) - def test_comment__str__(self): - comment = sample_dataclasses.comment("simple_1") - - should_print = """Comment(comment_created_at = 123, user = User(id = 123, display_name = abc, account_created_at = None, description = None, contributor_terms_agreed = None, img_url = None, roles = None, changesets_count = None, traces_count = None, blocks = None, ), action = opened, text = ABC, html = ABC, )""" - self.assertEqual(comment.__str__(), should_print) + def test__str__(self): + note = note_stub.OBJECT + should_print = "Note(id = 37970, latitude = 52.2722000, longitude = 20.4660000, note_created_at = 2023-02-26 13:37:26 UTC, open = True, comments = [Comment(comment_created_at='2023-02-26 13:37:26 UTC', user=User(id=18179, display_name='kwiatek_123 bot', account_created_at=None, description=None, contributor_terms_agreed=None, img_url=None, roles=None, changesets_count=None, traces_count=None, blocks=None), action='opened', text='test', html='')], )" + self.assertEqual(str(note), should_print) def test_to_from_dict(self): note = sample_dataclasses.note("full_1") From fd66e4997a225164b6619fd3dded4cd8e4833557 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:39:11 +0100 Subject: [PATCH 42/50] # pragma: no cover for asserts that should not happen --- src/osm_easy_api/api/endpoints/elements.py | 14 +++++++------- src/osm_easy_api/api/endpoints/gpx.py | 2 +- src/osm_easy_api/api/endpoints/notes.py | 6 +++--- src/osm_easy_api/diff/diff_parser.py | 4 ++-- src/osm_easy_api/utils/element_to_osm_object.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/osm_easy_api/api/endpoints/elements.py b/src/osm_easy_api/api/endpoints/elements.py index cfb94f8..2524b42 100644 --- a/src/osm_easy_api/api/endpoints/elements.py +++ b/src/osm_easy_api/api/endpoints/elements.py @@ -62,7 +62,7 @@ def get(self, element_type: Type[Node_Way_Relation], id: int) -> Node_Way_Relati object = element_to_osm_object(elem) return cast(element_type, object) - assert False, "No objects to parse!" + assert False, "No objects to parse!" # pragma: no cover def update(self, element: Node | Way | Relation, changeset_id: int) -> int: """Updates data for existing element. @@ -157,7 +157,7 @@ def version(self, element_type: Type[Node_Way_Relation], id: int, version: int) for elem in generator: if elem.tag in ("node", "way", "relation"): return cast(Node_Way_Relation, element_to_osm_object(elem)) - assert False, "[ERROR::API::ENDPOINTS::ELEMENTS::version] Cannot create an element." + assert False, "[ERROR::API::ENDPOINTS::ELEMENTS::version] Cannot create an element." # pragma: no cover def get_query(self, element_type: Type[Node_Way_Relation], ids: list[int]) -> list[Node_Way_Relation]: """Allows fetch multiple elements at once. @@ -251,21 +251,21 @@ def full(self, element_type: Type[Way_Relation], id: int) -> Way_Relation: for elem in generator: if elem.tag == "node": node = cast(Node, element_to_osm_object(elem)) - assert node.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" + assert node.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" # pragma: no cover nodes_dict.update({node.id: node}) if elem.tag == "way": way = cast(Way, element_to_osm_object(elem)) - assert way.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" + assert way.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" # pragma: no cover ways_dict.update({way.id: way}) if elem.tag == "relation" and element_name == "relation": relation = cast(Relation, element_to_osm_object(elem)) - assert relation.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" + assert relation.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" # pragma: no cover relations_dict.update({relation.id: relation}) for way in ways_dict.values(): for i in range(len(way.nodes)): node_id = way.nodes[i].id - assert node_id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" + assert node_id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {node}" # pragma: no cover node = nodes_dict[node_id] way.nodes[i] = deepcopy(node) @@ -274,7 +274,7 @@ def full(self, element_type: Type[Way_Relation], id: int) -> Way_Relation: members = relation.members for i in range(len(members)): element = members[i].element - assert element.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {element}" + assert element.id, f"[ERROR::API::ENDPOINTS::ELEMENTS::full] No id for {element}" # pragma: no cover if isinstance(element, Node): members[i] = Member(deepcopy(nodes_dict[element.id]), members[i].role) elif isinstance(element, Way): diff --git a/src/osm_easy_api/api/endpoints/gpx.py b/src/osm_easy_api/api/endpoints/gpx.py index 6341a71..2a6630a 100644 --- a/src/osm_easy_api/api/endpoints/gpx.py +++ b/src/osm_easy_api/api/endpoints/gpx.py @@ -33,7 +33,7 @@ def _xml_to_gpx_files(generator: Generator[ElementTree.Element, None, None]) -> timestamp = element.get("timestamp") latitude = element.get("lat") longitude = element.get("lon") - assert name and visibility and timestamp and latitude and longitude and description, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] missing members." + assert name and visibility and timestamp and latitude and longitude and description, "[ERROR::API::ENDPOINTS::GPX::GET_DETAILS] missing members." # pragma: no cover gpx_files.append(GpxFile( id=id, name=name, diff --git a/src/osm_easy_api/api/endpoints/notes.py b/src/osm_easy_api/api/endpoints/notes.py index 63d43dc..7cd839d 100644 --- a/src/osm_easy_api/api/endpoints/notes.py +++ b/src/osm_easy_api/api/endpoints/notes.py @@ -21,7 +21,7 @@ def _xml_to_notes_list(generator: Generator['ElementTree.Element', None, None]) for element in generator: match element.tag: case "id": - assert element.text, "[ERROR::API::ENDPOINTS::NOTE::_xml_to_note] No element.text in tag 'id'" + assert element.text, "[ERROR::API::ENDPOINTS::NOTE::_xml_to_note] No element.text in tag 'id'" # pragma: no cover temp_note.id = int(element.text) case "date_created": temp_note.note_created_at = element.text @@ -34,11 +34,11 @@ def _xml_to_notes_list(generator: Generator['ElementTree.Element', None, None]) for comment_tag in comment: # Comment specific data if comment_tag.tag == "date": - assert comment_tag.text, "[ERROR::API::ENDPOINTS::NOTE::_xml_to_note] No comment_tag.text in tag 'date'" + assert comment_tag.text, "[ERROR::API::ENDPOINTS::NOTE::_xml_to_note] No comment_tag.text in tag 'date'" # pragma: no cover temp_comment.comment_created_at = comment_tag.text # User specific data elif comment_tag.tag == "uid": - assert comment_tag.text, "[ERROR::API::ENDPOINTS::NOTE::_xml_to_note] No comment_tag.text in tag 'uid'" + assert comment_tag.text, "[ERROR::API::ENDPOINTS::NOTE::_xml_to_note] No comment_tag.text in tag 'uid'" # pragma: no cover temp_user.id = int(comment_tag.text) elif comment_tag.tag == "user": temp_user.display_name = comment_tag.text diff --git a/src/osm_easy_api/diff/diff_parser.py b/src/osm_easy_api/diff/diff_parser.py index b198188..203c688 100644 --- a/src/osm_easy_api/diff/diff_parser.py +++ b/src/osm_easy_api/diff/diff_parser.py @@ -38,7 +38,7 @@ def _is_correct(element: ElementTree.Element, tags: Tags | str) -> bool: matching_tags_counter += 1 return matching_tags_counter == len(tags) - raise ValueError("[ERROR::DIFF_PARSER::_IS_CORRECT] Unexpected return.") + assert False, ValueError("[ERROR::DIFF_PARSER::_IS_CORRECT] Unexpected return.") # pragma: no cover def _OsmChange_parser_generator(file: "gzip.GzipFile", sequence_number: str | None, required_tags: Tags | str = Tags()) -> Generator[tuple[Action, Node | Way | Relation] | Meta, None, None]: """Generator with elements in diff file. First yield will be Meta namedtuple. @@ -80,7 +80,7 @@ def _OsmChange_parser(file: "gzip.GzipFile", sequence_number: str | None, requir gen = _OsmChange_parser_generator(file, sequence_number, required_tags) # FIXME: Maybe OsmChange_parser_generator should return tuple(Meta, gen)? EDIT: I think Meta should be generated somewhere else meta = next(gen) - assert type(meta) == Meta, "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER] meta type is not equal to Meta." + assert isinstance(meta, Meta), "[ERROR::DIFF_PARSER::OSMCHANGE_PARSER] meta type is not equal to Meta." # pragma: no cover osmChange = OsmChange(meta.version, meta.generator, meta.sequence_number) for action, element in gen: # type: ignore (Next gen elements must be proper tuple type.) element = cast(Node | Way | Relation, element) diff --git a/src/osm_easy_api/utils/element_to_osm_object.py b/src/osm_easy_api/utils/element_to_osm_object.py index 297ed1a..f2b3060 100644 --- a/src/osm_easy_api/utils/element_to_osm_object.py +++ b/src/osm_easy_api/utils/element_to_osm_object.py @@ -12,4 +12,4 @@ def element_to_osm_object(element: 'Element') -> Node | Way | Relation: return Way._from_xml(element) case "relation": return Relation._from_xml(element) - case _: assert False, f"[ERROR::DIFF_PARSER::_ELEMENT_TO_OSM_OBJECT] Unknown element tag: {element.tag}" \ No newline at end of file + case _: assert False, f"[ERROR::DIFF_PARSER::_ELEMENT_TO_OSM_OBJECT] Unknown element tag: {element.tag}" # pragma: no cover \ No newline at end of file From 16c7dcca33adef2801efbb9f792276ffd535881b Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:47:40 +0100 Subject: [PATCH 43/50] Update test_api_elements.py --- tests/api/test_api_elements.py | 58 ++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/tests/api/test_api_elements.py b/tests/api/test_api_elements.py index 85742be..c3cdfa2 100644 --- a/tests/api/test_api_elements.py +++ b/tests/api/test_api_elements.py @@ -9,6 +9,9 @@ from osm_easy_api.api import exceptions as ApiExceptions class TestApiElements(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.API = Api(url="https://test.pl", access_token=TOKEN) @responses.activate def test_create(self): @@ -20,9 +23,8 @@ def test_create(self): }) def create_node(): - return api.elements.create(node, 123) - - api = Api(url="https://test.pl", access_token=TOKEN) + return self.API.elements.create(node, 123) + node = Node(latitude="123", longitude="321") self.assertEqual(create_node(), 1) @@ -72,9 +74,8 @@ def test_get(self): }) def get_node(): - return api.elements.get(Node, 123) + return self.API.elements.get(Node, 123) - api = Api(url="https://test.pl", access_token=TOKEN) node = get_node() self.assertEqual(str(node), should_be) @@ -120,11 +121,10 @@ def test_update(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - node = api.elements.get(Node, 123) + node = self.API.elements.get(Node, 123) node.latitude = "1" - self.assertEqual(api.elements.update(node, 1), 2) - node = api.elements.get(Node, 123) + self.assertEqual(self.API.elements.update(node, 1), 2) + node = self.API.elements.get(Node, 123) self.assertEqual(str(node), should_be) @responses.activate @@ -136,9 +136,8 @@ def test_delete(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) node = Node(123) - new_version = api.elements.delete(node, 333) + new_version = self.API.elements.delete(node, 333) self.assertEqual(new_version, 3) @responses.activate @@ -161,8 +160,7 @@ def test_history(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - history = api.elements.history(Node, 123) + history = self.API.elements.history(Node, 123) self.assertEqual(len(history), 4) self.assertEqual(history[3].user_id, 10688) self.assertEqual(history[3].visible, True) @@ -182,8 +180,7 @@ def test_version(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - version = api.elements.version(Node, 123, 4) + version = self.API.elements.version(Node, 123, 4) self.assertEqual(version.user_id, 10688) @responses.activate @@ -205,8 +202,7 @@ def test_get_query(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - nodes = api.elements.get_query(Node, [1, 2]) + nodes = self.API.elements.get_query(Node, [1, 2]) self.assertEqual(nodes[0].user_id, 12342) self.assertEqual(nodes[1].user_id, 10021) @@ -237,8 +233,7 @@ def test_relations(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - relations = api.elements.relations(Way, 111) + relations = self.API.elements.relations(Way, 111) self.assertEqual(relations[0].id, 79) self.assertEqual(relations[1].members[0].role, "outer") @@ -259,8 +254,7 @@ def test_ways(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - ways = api.elements.ways(111) + ways = self.API.elements.ways(111) self.assertEqual(ways[0].id, 5638) self.assertEqual(ways[1].nodes[0].id, 1368) @@ -452,8 +446,7 @@ def test_full(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - relation = api.elements.full(Relation, 226) + relation = self.API.elements.full(Relation, 226) self.assertEqual(relation.id, 226) self.assertEqual(relation.members[1].element.id, 6178) self.assertEqual(relation.members[2].element.id, 6179) @@ -499,7 +492,7 @@ def test_full(self): "status": 200 }) - way = api.elements.full(Way, 226) + way = self.API.elements.full(Way, 226) self.assertEqual(way.id, 226) self.assertEqual(way.nodes[0].id, 5281) self.assertEqual(way.nodes[0].latitude, "58.5769849") @@ -516,6 +509,17 @@ def test_no_uid(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - history = api.elements.history(Node, 123) - self.assertEqual(history[0].user_id, -1) \ No newline at end of file + history = self.API.elements.history(Node, 123) + self.assertEqual(history[0].user_id, -1) + + @responses.activate + def test_redaction(self): + URL = "https://test.pl/api/0.6/node/123/4/redact?redaction=13" + responses.add(**{ + "method": responses.POST, + "url": URL, + "status": 200 + }) + + self.API.elements.redaction(Node, 123, 4, 13) + self.assertTrue(responses.assert_call_count(URL, 1)) \ No newline at end of file From 7098282ca048b0919be8b2b86156fc2cc2eb4ec6 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:37:50 +0100 Subject: [PATCH 44/50] test_diff --- tests/diff/test_diff.py | 31 +++++++++++++++++++++++--- tests/fixtures/compare_files.py | 23 +++++++++++++++++++ tests/utils/test_write_gzip_to_file.py | 25 ++------------------- 3 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/compare_files.py diff --git a/tests/diff/test_diff.py b/tests/diff/test_diff.py index 8fd3dbe..d900be4 100644 --- a/tests/diff/test_diff.py +++ b/tests/diff/test_diff.py @@ -1,10 +1,11 @@ import unittest import responses import os +import filecmp from osm_easy_api.diff import Diff, Frequency from osm_easy_api.data_classes import OsmChange, Node, Way, Relation, Action - +from ..fixtures.compare_files import _compare_files class TestDiff(unittest.TestCase): @classmethod @@ -118,6 +119,9 @@ def test_diff_get_generator(self): @responses.activate def test_diff_get(self): + FILE_FROM = os.path.join("tests", "fixtures", "hour.xml.gz") + FILE_TO = os.path.join("tests", "fixtures", "test_diff_get_file_to.xml.gz") + def check_osm_change(osm_change: OsmChange): self.assertEqual(osm_change.get(Node, Action.CREATE).__len__(), 2) self.assertEqual(osm_change.get(Node, Action.MODIFY).__len__(), 14) @@ -134,7 +138,7 @@ def check_osm_change(osm_change: OsmChange): self.assertEqual(osm_change.get(Relation, Action.DELETE).__len__(), 0) self.assertEqual(osm_change.get(Relation, Action.NONE).__len__(), 0) - with open(os.path.join("tests", "fixtures", "hour.xml.gz"), "rb") as f: + with open(FILE_FROM, "rb") as f: responses.add(**{ "method": responses.GET, "url": "https://test.pl/minute/005/315/422.osc.gz", @@ -150,7 +154,7 @@ def check_osm_change(osm_change: OsmChange): self.assertTrue(responses.assert_call_count("https://test.pl/minute/state.txt", 0)) self.assertTrue(responses.assert_call_count("https://test.pl/minute/005/315/422.osc.gz", 1)) - with open(os.path.join("tests", "fixtures", "hour.xml.gz"), "rb") as f: + with open(FILE_FROM, "rb") as f: responses.add(**{ "method": responses.GET, "url": "https://test.pl/minute/005/315/422.osc.gz", @@ -172,3 +176,24 @@ def check_osm_change(osm_change: OsmChange): check_osm_change(osm_change) self.assertTrue(responses.assert_call_count("https://test.pl/minute/state.txt", 1)) self.assertTrue(responses.assert_call_count("https://test.pl/minute/005/315/422.osc.gz", 2)) + + osm_change = DIFF.get(file_from=FILE_FROM, generator=False) + assert isinstance(osm_change, OsmChange) + check_osm_change(osm_change) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/state.txt", 1)) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/005/315/422.osc.gz", 2)) + + with open(FILE_FROM, "rb") as f: + responses.add(**{ + "method": responses.GET, + "url": "https://test.pl/minute/005/315/422.osc.gz", + "body": f, + "status": 200 + }) + osm_change = DIFF.get(file_to=FILE_TO, sequence_number=self.SEQUENCE_NUMBER, generator=False) + assert isinstance(osm_change, OsmChange) + check_osm_change(osm_change) + self.assertTrue(_compare_files(FILE_FROM, FILE_TO)) + os.remove(FILE_TO) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/state.txt", 1)) + self.assertTrue(responses.assert_call_count("https://test.pl/minute/005/315/422.osc.gz", 3)) diff --git a/tests/fixtures/compare_files.py b/tests/fixtures/compare_files.py new file mode 100644 index 0000000..6726413 --- /dev/null +++ b/tests/fixtures/compare_files.py @@ -0,0 +1,23 @@ +import gzip +from xml.etree import ElementTree + +def _compare_files(first_path, second_path): + """Compare two files and returns False if files don't have similar content. Otherwise returns True.""" + def _parse_file(file): + for e, i in ElementTree.iterparse(file): + yield i + + first = gzip.GzipFile(first_path) + second = gzip.GzipFile(second_path) + + one = _parse_file(first) + two = _parse_file(second) + for i in one: + n = next(two) + if i.tag != n.tag or i.attrib != n.attrib: + first.close() + second.close() + return False + first.close() + second.close() + return True \ No newline at end of file diff --git a/tests/utils/test_write_gzip_to_file.py b/tests/utils/test_write_gzip_to_file.py index 7f1152d..c3d3a90 100644 --- a/tests/utils/test_write_gzip_to_file.py +++ b/tests/utils/test_write_gzip_to_file.py @@ -1,32 +1,11 @@ import unittest import gzip import os -from xml.etree import ElementTree +from ..fixtures.compare_files import _compare_files from osm_easy_api.utils import write_gzip_to_file class TestMiscWriteGzipToFile(unittest.TestCase): - def _parse_file(self, file): - for e, i in ElementTree.iterparse(file): - yield i - - def _compare_files(self, first_path, second_path): - """Compare two files and returns False if files don't have similar content. Otherwise returns True.""" - first = gzip.GzipFile(first_path) - second = gzip.GzipFile(second_path) - - one = self._parse_file(first) - two = self._parse_file(second) - for i in one: - n = next(two) - if i.tag != n.tag or i.attrib != n.attrib: - first.close() - second.close() - return False - first.close() - second.close() - return True - def test_write(self): f_from_path = os.path.join("tests", "fixtures", "hour.xml.gz") f_to_path = os.path.join("tests", "fixtures", "write_gzip_to_file_to.xml.gz") @@ -34,5 +13,5 @@ def test_write(self): write_gzip_to_file(f_from, f_to_path) - self.assertTrue(self._compare_files(f_from_path, f_to_path)) + self.assertTrue(_compare_files(f_from_path, f_to_path)) os.remove(f_to_path) \ No newline at end of file From bbf33b68ab0123a61d7fa4b3dac12f10d57df324 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:51:27 +0100 Subject: [PATCH 45/50] test_api_changeset --- src/osm_easy_api/api/endpoints/changeset.py | 1 - tests/api/test_api_changeset.py | 66 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 5564917..7d74588 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -242,7 +242,6 @@ def upload(self, changeset_id: int, osmChange: OsmChange, make_osmChange_valid: - **400 -> `osm_easy_api.api.exceptions.ErrorWhenParsingXML`:** Incorrect OsmChange object. Maybe missing elements attributes. - **404 -> `osm_easy_api.api.exceptions.IdNotFoundError`:** No changeset with provided ID or can't find element with ID in OsmChange. - **409 -> `osm_easy_api.api.exceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor`:** Changeset already closed or you are not an author. - - **400 -> `osm_easy_api.api.exceptions.ErrorWhenParsingXML`:** Incorrect OsmChange object. Maybe missing elements attributes. OTHER -> ValueError: Unexpected but correct error. """ self.outer._request( diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index 1cd98b1..635d2f8 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -5,8 +5,9 @@ from ..fixtures.default_variables import TOKEN from osm_easy_api.api import Api -from osm_easy_api.data_classes import Changeset, Tags, Node +from osm_easy_api.data_classes import Changeset, Tags, Node, OsmChange, Action from osm_easy_api.api import exceptions as ApiExceptions +from ..fixtures import sample_dataclasses class TestApiChangeset(unittest.TestCase): @@ -21,6 +22,7 @@ def test_create(self): api = Api(url="https://test.pl", access_token=TOKEN) self.assertEqual(api.changeset.create("ABC"), 111) + self.assertEqual(api.changeset.create("ABC", tags=Tags({"alfa": "beta"})), 111) @responses.activate def test_get(self): @@ -91,7 +93,7 @@ def test_get_query(self): """ responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/changesets/?user=18179&limit=100", + "url": "https://test.pl/api/0.6/changesets/?user=18179&changesets=111,222&limit=100", "body": body, "status": 200 }) @@ -107,7 +109,7 @@ def test_get_query(self): Tags({"comment": "Upload relation test"}) ) - testing_changeset = api.changeset.get_query(user_id=18179)[1] + testing_changeset = api.changeset.get_query(user_id=18179, changesets_id=[111, 222])[1] self.assertEqual(testing_changeset.id, changeset.id) self.assertEqual(testing_changeset.timestamp, changeset.timestamp) self.assertEqual(testing_changeset.open, changeset.open) @@ -323,4 +325,60 @@ def test_download(self): def download(): return api.changeset.download(111) - self.assertRaises(ApiExceptions.IdNotFoundError, download) \ No newline at end of file + self.assertRaises(ApiExceptions.IdNotFoundError, download) + + @responses.activate + def test_upload(self): + URL = "https://test.pl/api/0.6/changeset/123/upload" + + osmChange = OsmChange("0.1", "unittest", "123") + osmChange.add(sample_dataclasses.node("simple_1")) + should_print = "OsmChange(version=0.1, generator=unittest, sequence_number=123. Node: Create(0), Modify(0), Delete(0), None(1). Way: Create(0), Modify(0), Delete(0), None(0). Relation: Create(0), Modify(0), Delete(0), None(0)." + self.assertEqual(str(osmChange), should_print) + osmChange.add(sample_dataclasses.node("simple_2"), Action.MODIFY) + osmChange.add(sample_dataclasses.way("simple_1"), Action.MODIFY) + + api = Api("https://test.pl") + def upload(): + return api.changeset.upload(123, osmChange) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "body": None, # TODO: To be supported + "status": 200 + }) + upload() + self.assertTrue(responses.assert_call_count(URL, 1)) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "status": 400 + }) + self.assertRaises(ApiExceptions.ErrorWhenParsingXML, upload) + self.assertTrue(responses.assert_call_count(URL, 2)) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "status": 404 + }) + self.assertRaises(ApiExceptions.IdNotFoundError, upload) + self.assertTrue(responses.assert_call_count(URL, 3)) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "status": 409 + }) + self.assertRaises(ApiExceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor, upload) + self.assertTrue(responses.assert_call_count(URL, 4)) + + responses.add(**{ + "method": responses.POST, + "url": URL, + "status": 999 + }) + self.assertRaises(ValueError, upload) + self.assertTrue(responses.assert_call_count(URL, 5)) \ No newline at end of file From fa0e642b9c8ac1dab5d3a60b29540be38ba41548 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:35:52 +0100 Subject: [PATCH 46/50] test_api_changeset --- tests/api/test_api_changeset.py | 194 ++++--------------------- tests/fixtures/stubs/changeset_stub.py | 27 ++++ 2 files changed, 58 insertions(+), 163 deletions(-) create mode 100644 tests/fixtures/stubs/changeset_stub.py diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index 635d2f8..3288b19 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -8,8 +8,15 @@ from osm_easy_api.data_classes import Changeset, Tags, Node, OsmChange, Action from osm_easy_api.api import exceptions as ApiExceptions from ..fixtures import sample_dataclasses +from ..fixtures.stubs import changeset_stub + +def _are_changesets_equal(first: Changeset, second: Changeset): + return first.to_dict() == second.to_dict() class TestApiChangeset(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.API = Api(url="https://test.pl", access_token=TOKEN) @responses.activate def test_create(self): @@ -20,54 +27,20 @@ def test_create(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - self.assertEqual(api.changeset.create("ABC"), 111) - self.assertEqual(api.changeset.create("ABC", tags=Tags({"alfa": "beta"})), 111) + self.assertEqual(self.API.changeset.create("ABC"), 111) + self.assertEqual(self.API.changeset.create("ABC", tags=Tags({"alfa": "beta"})), 111) @responses.activate def test_get(self): - body = """ - - - - - - - - abc - - - - - """ responses.add(**{ "method": responses.GET, "url": "https://test.pl/api/0.6/changeset/111?include_discussion=true", - "body": body, + "body": changeset_stub.XML_RESPONSE_BODY, "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) - changeset = Changeset( - 111, - "2022-12-26T13:33:40Z", - False, - "18179", - "1", - "0", - Tags({"testing": "yes", "created_by": "osm-python-api", "comment": "aaa"}), - [{"date": "2022-12-26T14:22:22Z", "user_id": "18179", "text": "abc"}] - ) - - testing_changeset = api.changeset.get(111, True) - self.assertEqual(testing_changeset.id, changeset.id) - self.assertEqual(testing_changeset.timestamp, changeset.timestamp) - self.assertEqual(testing_changeset.open, changeset.open) - self.assertEqual(testing_changeset.user_id, changeset.user_id) - self.assertEqual(testing_changeset.comments_count, changeset.comments_count) - self.assertEqual(testing_changeset.changes_count, changeset.changes_count) - self.assertEqual(testing_changeset.tags, changeset.tags) - self.assertEqual(testing_changeset.discussion, changeset.discussion) + testing_changeset = self.API.changeset.get(111, True) + self.assertTrue(_are_changesets_equal(testing_changeset, changeset_stub.OBJECT)) responses.add(**{ "method": responses.GET, @@ -76,7 +49,7 @@ def test_get(self): }) def get(): - return api.changeset.get(111, True) + return self.API.changeset.get(111, True) self.assertRaises(ApiExceptions.IdNotFoundError, get) @@ -98,7 +71,6 @@ def test_get_query(self): "status": 200 }) - api = Api(url="https://test.pl", access_token=TOKEN) changeset = Changeset( 222, "2023-01-10T16:48:58Z", @@ -109,21 +81,10 @@ def test_get_query(self): Tags({"comment": "Upload relation test"}) ) - testing_changeset = api.changeset.get_query(user_id=18179, changesets_id=[111, 222])[1] - self.assertEqual(testing_changeset.id, changeset.id) - self.assertEqual(testing_changeset.timestamp, changeset.timestamp) - self.assertEqual(testing_changeset.open, changeset.open) - self.assertEqual(testing_changeset.user_id, changeset.user_id) - self.assertEqual(testing_changeset.comments_count, changeset.comments_count) - self.assertEqual(testing_changeset.changes_count, changeset.changes_count) - self.assertEqual(testing_changeset.tags, changeset.tags) + testing_changeset = self.API.changeset.get_query(user_id=18179, changesets_id=[111, 222])[1] + self.assertTrue(_are_changesets_equal(testing_changeset, changeset)) - body = """ - - - - - """ + body = changeset_stub.XML_RESPONSE_BODY responses.add(**{ "method": responses.GET, @@ -131,104 +92,29 @@ def test_get_query(self): "body": body, "status": 200 }) - changeset_list = api.changeset.get_query(user_id=18179, limit=1) + changeset_list = self.API.changeset.get_query(user_id=18179, limit=1) self.assertEqual(changeset_list.__len__(), 1) - responses.add(**{ - "method": responses.GET, - "url": "https://test.pl/api/0.6/changeset/111?include_discussion=true", - "status": 404 - }) - - def get(): - return api.changeset.get(111, True) - - self.assertRaises(ApiExceptions.IdNotFoundError, get) - @responses.activate def test_update(self): - body = """ - - - - - - - - abc - - - - - """ responses.add(**{ "method": responses.PUT, "url": "https://test.pl/api/0.6/changeset/111", - "body": body, + "body": changeset_stub.XML_RESPONSE_BODY, "status": 200 }) - changeset = Changeset( - 111, - "2022-12-26T13:33:40Z", - False, - "18179", - "1", - "0", - Tags({"testing": "yes", "created_by": "osm-python-api", "comment": "aaa"}), - [{"date": "2022-12-26T14:22:22Z", "user_id": "18179", "text": "abc"}] - ) + testing_changeset = self.API.changeset.update(111, "BBB") + self.assertTrue(_are_changesets_equal(testing_changeset, changeset_stub.OBJECT)) - api = Api(url="https://test.pl", access_token=TOKEN) - - testing_changeset = api.changeset.update(111, "BBB") - self.assertEqual(testing_changeset.id, changeset.id) - self.assertEqual(testing_changeset.timestamp, changeset.timestamp) - self.assertEqual(testing_changeset.open, changeset.open) - self.assertEqual(testing_changeset.user_id, changeset.user_id) - self.assertEqual(testing_changeset.comments_count, changeset.comments_count) - self.assertEqual(testing_changeset.changes_count, changeset.changes_count) - self.assertEqual(testing_changeset.tags, changeset.tags) - self.assertEqual(testing_changeset.discussion, changeset.discussion) - - body = """ - - - - - - - - abc - - - - - """ - responses.add(**{ - "method": responses.PUT, - "url": "https://test.pl/api/0.6/changeset/111", - "body": body, - "status": 200 - }) - testing_changeset = api.changeset.update(111, "BBB" , Tags({"testing": "no"})) - new_tags = copy(changeset.tags) - new_tags.update({"testing": "no"}) - self.assertEqual(testing_changeset.id, changeset.id) - self.assertEqual(testing_changeset.timestamp, changeset.timestamp) - self.assertEqual(testing_changeset.open, changeset.open) - self.assertEqual(testing_changeset.user_id, changeset.user_id) - self.assertEqual(testing_changeset.comments_count, changeset.comments_count) - self.assertEqual(testing_changeset.changes_count, changeset.changes_count) - self.assertEqual(testing_changeset.tags, new_tags) - self.assertEqual(testing_changeset.discussion, changeset.discussion) + testing_changeset = self.API.changeset.update(111, "BBB" , Tags({"testing": "yes"})) + self.assertTrue(_are_changesets_equal(testing_changeset, changeset_stub.OBJECT)) def update(): - return api.changeset.update(111, "BBB") + return self.API.changeset.update(111, "BBB") responses.add(**{ "method": responses.PUT, "url": "https://test.pl/api/0.6/changeset/111", - "body": body, "status": 404 }) self.assertRaises(ApiExceptions.IdNotFoundError, update) @@ -236,38 +122,26 @@ def update(): responses.add(**{ "method": responses.PUT, "url": "https://test.pl/api/0.6/changeset/111", - "body": body, "status": 409 }) self.assertRaises(ApiExceptions.ChangesetAlreadyClosedOrUserIsNotAnAuthor, update) @responses.activate def test_close(self): - responses.add(**{ - "method": responses.PUT, - "url": "https://test.pl/api/0.6/changeset/create", - "body": "111", - "status": 200 - }) - + def close(): + return self.API.changeset.close(111) + responses.add(**{ "method": responses.PUT, "url": "https://test.pl/api/0.6/changeset/111/close", "body": "111", "status": 200 }) - - api = Api(url="https://test.pl", access_token=TOKEN) - changeset_id = api.changeset.create("ABC") - def close(): - return api.changeset.close(changeset_id) - close() responses.add(**{ "method": responses.PUT, "url": "https://test.pl/api/0.6/changeset/111/close", - "body": "111", "status": 404 }) self.assertRaises(ApiExceptions.IdNotFoundError, close) @@ -282,6 +156,9 @@ def close(): @responses.activate def test_download(self): + def download(): + return self.API.changeset.download(111) + body = """ @@ -302,10 +179,7 @@ def test_download(self): "body": body, "status": 200 }) - - api = Api("https://test.pl") - - generator = api.changeset.download(111) + generator = download() second_node = None for action, element in generator: @@ -318,13 +192,8 @@ def test_download(self): responses.add(**{ "method": responses.GET, "url": "https://test.pl/api/0.6/changeset/111/download", - "body": body, "status": 404 }) - - def download(): - return api.changeset.download(111) - self.assertRaises(ApiExceptions.IdNotFoundError, download) @responses.activate @@ -338,9 +207,8 @@ def test_upload(self): osmChange.add(sample_dataclasses.node("simple_2"), Action.MODIFY) osmChange.add(sample_dataclasses.way("simple_1"), Action.MODIFY) - api = Api("https://test.pl") def upload(): - return api.changeset.upload(123, osmChange) + return self.API.changeset.upload(123, osmChange) responses.add(**{ "method": responses.POST, diff --git a/tests/fixtures/stubs/changeset_stub.py b/tests/fixtures/stubs/changeset_stub.py new file mode 100644 index 0000000..1ebc180 --- /dev/null +++ b/tests/fixtures/stubs/changeset_stub.py @@ -0,0 +1,27 @@ +from osm_easy_api.data_classes import Changeset, Tags + +XML_RESPONSE_BODY = """ + + + + + + + + abc + + + + + """ + +OBJECT = Changeset( + id=111, + timestamp="2022-12-26T13:33:40Z", + open=False, + user_id="18179", + comments_count="1", + changes_count="0", + tags=Tags({"testing": "yes", "created_by": "osm-python-api", "comment": "aaa"}), + discussion=[{"date": "2022-12-26T14:22:22Z", "user_id": "18179", "text": "abc"}] + ) \ No newline at end of file From d8ec91e30f6a84be08711ecfa96a2c9675ffcc0d Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:47:52 +0100 Subject: [PATCH 47/50] `order` parameter --- CHANGELOG.md | 25 +++++++++++---------- src/osm_easy_api/api/endpoints/changeset.py | 5 ++++- tests/api/test_api_changeset.py | 4 ++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e978e32..10f59db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `gpx.get_details()` endpoint. - `gpx.get_file()` endpoint. - `gpx.list_details()` endpoint. +- `order` parameter in `changeset.get_query()`. ### Fixed - Types in `elements` endpoint. @@ -44,20 +45,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Most `# type: ignore`. - `NotAModerator` exception. It is now replaced by `Forbidden` exception. -## [2.2.0] +## [2.2.0] - 2024-02-23 ### Added - Exception for `410` status code in notes endpoint. -## [2.1.1] +## [2.1.1] - 2024-01-04 ### Fixed - Percent-encoding was not applied on texts entered by the user. [#22](https://github.com/docentYT/osm_easy_api/issues/22) -## [2.1.0] +## [2.1.0] - 2023-09-06 ### Added - `TooManyRequests` exception. - Support for `429` status code in `api.changeset.discussion.comment()`. -## [2.0.0] +## [2.0.0] - 2023-08-29 ### Added - Missing status code handling in `notes.get()`. - Support for hide note endpoint. @@ -74,27 +75,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `EmptyResult` api exception, which was used in endpoints `notes`, `user` and `changeset`. From now on when the results are empty an empty list will be returned. - Unused imports. -## [1.1.1] +## [1.1.1] - 2023-08-03 ### Fixed - Corrected character when adding parameters in endpoint `api.notes.get_bbox()` (from `?` to `&`). - Fixed the `limit` parameter restrictions in `api.notes.get_bbox()` documentation. -## [1.1.0] +## [1.1.0] - 2023-08-28 ### Added - `limit` parameter for `api.changeset.get_query()` endpoint. ### Fixed - Corrected character when adding parameters in endpoint `api.changeset.get_query()` (from `;` to `&`). -## [1.0.2] +## [1.0.2] - 2023-07-26 ### Fixed - Auth problems in API when using characters unsupported by latin-1 codec. -## [1.0.1] +## [1.0.1] - 2023-07-24 ### Fixed - `api.notes.create()` created only anonymous notes. -## [1.0.0] +## [1.0.0] - 2023-06-18 ### Added - `to_xml()` method in `OsmChange`. - `upload()` method in `changeset` `endpoint` has new optional arguments. @@ -104,7 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Private `_to_xml()` method in `OsmChange` is now static. -## [0.4.2] +## [0.4.2] - 2023-06-11 ### Changed - Order of elements in xml generated by `Way._to_xml()`. First tags, then nodes. @@ -114,11 +115,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deleted disused variable in `Node._to_xml()`. - Fixed incorrect relation parsing of data recived by `full` endpoint. -## [0.4.1] +## [0.4.1] - 2023-05-23 ### Changed - Updated `requests` from `2.28.1` to `2.31.0`. -## [0.4.0] +## [0.4.0] - 2023-05-03 ### Added - `to_dict()` method and `from_dict()` class method to `Note`. - `to_dict()` method and `from_dict()` class method to `Comment`. diff --git a/src/osm_easy_api/api/endpoints/changeset.py b/src/osm_easy_api/api/endpoints/changeset.py index 7d74588..cc3bffa 100644 --- a/src/osm_easy_api/api/endpoints/changeset.py +++ b/src/osm_easy_api/api/endpoints/changeset.py @@ -106,6 +106,7 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ time_one: str | None = None, time_two: str | None = None, open: bool = False, closed: bool = False, changesets_id: list[int] | None = None, + order: str = "newest", limit: int = 100 ) -> list[Changeset]: """Get changesets with given criteria. @@ -122,6 +123,7 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ open (bool, optional): Find only open changesets. Defaults to False. closed (bool, optional): Find only closed changesets. Defaults to False. changesets_id (list[int] | None, optional): List of ids to search for. Defaults to None. + order (str, optional): If 'newest', sort newest changesets first. If 'oldest', reverse order. Defaults to newest. limit (int, optional): Specifies the maximum number of changesets returned. Must be between 1 and 100. Defaults to 100. Custom exceptions: @@ -144,7 +146,8 @@ def get_query(self, left: float | None = None, bottom: float | None = None, righ for id in changesets_id: param += f",{id}" param += "&" - param+=f"limit={limit}" + param+=f"order={order}" + param+=f"&limit={limit}" generator = self.outer._request_generator( method=self.outer._RequestMethods.GET, diff --git a/tests/api/test_api_changeset.py b/tests/api/test_api_changeset.py index 3288b19..9a36a5a 100644 --- a/tests/api/test_api_changeset.py +++ b/tests/api/test_api_changeset.py @@ -66,7 +66,7 @@ def test_get_query(self): """ responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/changesets/?user=18179&changesets=111,222&limit=100", + "url": "https://test.pl/api/0.6/changesets/?user=18179&changesets=111,222&order=newest&limit=100", "body": body, "status": 200 }) @@ -88,7 +88,7 @@ def test_get_query(self): responses.add(**{ "method": responses.GET, - "url": "https://test.pl/api/0.6/changesets/?user=18179&limit=1", + "url": "https://test.pl/api/0.6/changesets/?user=18179&order=newest&limit=1", "body": body, "status": 200 }) From 76097a5405a0325de961cc619983650865f89b67 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:49:21 +0100 Subject: [PATCH 48/50] Update coverage-badge.svg --- coverage-badge.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage-badge.svg b/coverage-badge.svg index c149003..3438732 100644 --- a/coverage-badge.svg +++ b/coverage-badge.svg @@ -9,13 +9,13 @@ - + coverage coverage - 90% - 90% + 97% + 97% From 59e92b4dfc4d525800a663bc9882041370931384 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:51:52 +0100 Subject: [PATCH 49/50] API.txt remove --- API.txt | 74 ------------------------------------------------------- README.md | 5 +--- 2 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 API.txt diff --git a/API.txt b/API.txt deleted file mode 100644 index 3428648..0000000 --- a/API.txt +++ /dev/null @@ -1,74 +0,0 @@ -MISC ✅ -✅ GET /api/versions - API versions supported by this instance. -✅ GET /api/capabilities - Capabilities and limitations of the current API. - -✅ GET /api/0.6/map?bbox=Left,Bottom,Right,Top - Retrieving map data by bounding box. -✅ GET /api/0.6/permissions - Returns the permissions granted to the current API connection. - -CHANGESETS ✅ -✅ PUT /api/0.6/changeset/create - Creates changeset -✅ GET /api/0.6/changeset/#id?include_discussion=true - Get changeset by id -✅ PUT /api/0.6/changeset/#id - Update changeset -✅ PUT /api/0.6/changeset/#id/close - Close changeset -✅ GET /api/0.6/changeset/#id/download - OsmChange (nice) for chosen changeset -✅ POST /api/0.6/changeset/#id/upload - Upload OsmChange - -✅ GET /api/0.6/changesets - Get changesets for provided parameters - -changeset discussion ✅ -✅ POST /api/0.6/changeset/#id/comment - Add a comment to a closed changeset. -✅ POST /api/0.6/changeset/#id/subscribe - Subscribe to receive notifications for a new comments. -✅ POST /api/0.6/changeset/#id/unsubscribe - Unsubscribe to stop receiving notifinactions for a new comments. -✅ MO: POST /api/0.6/changeset/comment/#comment_id/hide - Hide comment from changeset. -✅ MO: POST /api/0.6/changeset/comment/#comment_id/unhide - Unhide comment from changeset. - -ELEMENTS ✅ -✅ PUT /api/0.6/[node|way|relation]/create - Create new element. -✅ GET /api/0.6/[node|way|relation]/#id - Get element with id. -✅ PUT /api/0.6/[node|way|relation]/#id - Update element with id. -✅ DELETE /api/0.6/[node|way|relation]/#id - Delete element with id. -✅ GET /api/0.6/[node|way|relation]/#id/history - Get old versions of an element. -✅ GET /api/0.6/[node|way|relation]/#id/#version - Get specific version of element. -✅ GET /api/0.6/[nodes|ways|relations]?#parameters - Get multpile elements on one request. -✅ GET /api/0.6/[node|way|relation]/#id/relations - Get relations for element. -✅ GET /api/0.6/node/#id/ways - Get ways for node. -✅ GET /api/0.6/[way|relation]/#id/full - Get ALL DATA for element (way => all nodes with data, relation => all members) -✅ MO: POST /api/0.6/[node|way|relation]/#id/#version/redact?redaction=#redaction_id - Hide element with data privacy or copyright infringements. - -GPS TRACES -✅ GET /api/0.6/trackpoints?bbox=left,bottom,right,top&page=pageNumber - Get GPS tracks that are inside a given bbox -✅ POST /api/0.6/gpx/create - Create new GPS trace from GPX file -✅ OO: PUT /api/0.6/gpx/#id - Update gps trace -✅ OO: DELETE /api/0.6/gpx/#id - Delete gps trace -✅ GET /api/0.6/gpx/#id/details - Get osm data about GPS trace OO: if private -✅ GET /api/0.6/gpx/#id/data - Get GPX file for trace OO: if private -✅ OO: GET /api/0.6/user/gpx_files - Get all traces for owner account - -USERS ✅ -✅ GET /api/0.6/user/#id - Get user data by id. -✅ GET /api/0.6/users?users=#id1,#id2,...,#idn - Get user data for mulptile users on one request. -✅ CL: GET /api/0.6/user/details - Get user data -✅ CL: GET /api/0.6/user/preferences - Get user preferences -✅ CL: PUT /api/0.6/user/preferences - Update user preferences -✅ CL: DELETE /api/0.6/user/preferences - Delete user preference - -NOTES -✅ GET /api/0.6/notes?bbox=Left,Bottom,Right,Top - Get all notes in bbox -✅ GET /api/0.6/notes/#id - Get note by id -✅ POST /api/0.6/notes - Create new note -✅ POST /api/0.6/notes/#id/comment - Create new comment to note -✅ POST /api/0.6/notes/#id/close - Close note -✅ POST /api/0.6/notes/#id/reopen - Reopen note -✅ MO: DELETE /api/0.6/notes/#id/ - Hide note. -✅ GET /api/0.6/notes/search - Search for notes -X GET /api/0.6/notes/feed?bbox=Left,Bottom,Right,Top - Get RSS feed for notes in bbox - - -* -MO = Moderator Only -OO = Owner account only -CL = Current logged user - -✅ - Done -❗ - No error code handling -❓ - No tests \ No newline at end of file diff --git a/README.md b/README.md index 710edc2..ee3559c 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,12 @@

Me on OpenStreetMap

-> Python package for parsing osm diffs and communicating with the OpenStreetMap api. See `API.txt` for list of supported endpoints. +> Python package for parsing osm diffs and communicating with the OpenStreetMap api. ### What's the point of this package? This package was created to provide an easy way to create automated scripts and programs that use diff and/or osm api. The main advantage is the classes (data_classes) that provide data of elements (node, way, relation, OsmChange, etc.) in a readable way and the possibility to use them in diff and api without worrying about missing data or dictionaries. You can easily find nodes in diff, add a tag to them and send the corrected version to osm. -### What next? -The plan is to add support for gpx traces, rss support and overpass api. - ## Installation Works on python >= 3.10. (Due to new typehints standard) From 659866200955760e330c20098a7846b1e99d9058 Mon Sep 17 00:00:00 2001 From: docentYT <63965954+docentYT@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:52:39 +0100 Subject: [PATCH 50/50] v3.0.0 --- CHANGELOG.md | 2 +- src/osm_easy_api/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f59db..d4fa29a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [3.0.0] - 2024-03-19 ### Added - Support for `oAuth2`: `access_token` parameter in `Api` class constructor. - `Unauthorized` exception. (No access token.) diff --git a/src/osm_easy_api/__init__.py b/src/osm_easy_api/__init__.py index c3624af..eff3272 100644 --- a/src/osm_easy_api/__init__.py +++ b/src/osm_easy_api/__init__.py @@ -1,8 +1,6 @@ """Python package for parsing osm diffs and communicating with the OpenStreetMap api.""" -VERSION = "2.2.0" +VERSION = "3.0.0" -# from .data_classes import * from . import data_classes from . import diff - from . import api \ No newline at end of file