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 @@
+
+
+
+
+
+ 2024-01-29T10:44:44Z
+
+
+ 2024-01-29T10:44:45Z
+
+
+ 2024-01-29T10:44:46Z
+
+
+ 2024-01-29T10:44:47Z
+
+
+ 2024-01-29T10:44:48Z
+
+
+ 2024-01-29T10:44:49Z
+
+
+ 2024-01-29T10:44:50Z
+
+
+ 2024-01-29T10:44:51Z
+
+
+ 2024-01-29T10:44:52Z
+
+
+ 2024-01-29T10:44:53Z
+
+
+ 2024-01-29T10:44:54Z
+
+
+ 2024-01-29T10:44:55Z
+
+
+ 2024-01-29T10:44:56Z
+
+
+ 2024-01-29T10:44:57Z
+
+
+ 2024-01-29T10:44:58Z
+
+
+ 2024-01-29T10:44:59Z
+
+
+ 2024-01-29T10:45:00Z
+
+
+ 2024-01-29T10:45:01Z
+
+
+ 2024-01-29T10:45:02Z
+
+
+ 2024-01-29T10:45:03Z
+
+
+ 2024-01-29T10:45:04Z
+
+
+ 2024-01-29T10:45:05Z
+
+
+ 2024-01-29T10:45:06Z
+
+
+ 2024-01-29T10:45:07Z
+
+
+ 2024-01-29T10:45:08Z
+
+
+ 2024-01-29T10:45:09Z
+
+
+ 2024-01-29T10:45:10Z
+
+
+ 2024-01-29T10:45:11Z
+
+
+ 2024-01-29T10:45:12Z
+
+
+ 2024-01-29T10:45:13Z
+
+
+ 2024-01-29T10:45:14Z
+
+
+ 2024-01-29T10:45:15Z
+
+
+ 2024-01-29T10:45:16Z
+
+
+ 2024-01-29T10:45:17Z
+
+
+ 2024-01-29T10:45:18Z
+
+
+ 2024-01-29T10:45:19Z
+
+
+ 2024-01-29T10:45:20Z
+
+
+ 2024-01-29T10:45:21Z
+
+
+ 2024-01-29T10:45:22Z
+
+
+ 2024-01-29T10:45:23Z
+
+
+ 2024-01-29T10:45:24Z
+
+
+ 2024-01-29T10:45:25Z
+
+
+ 2024-01-29T10:45:26Z
+
+
+ 2024-01-29T10:45:27Z
+
+
+ 2024-01-29T10:45:28Z
+
+
+ 2024-01-29T10:45:29Z
+
+
+ 2024-01-29T10:45:30Z
+
+
+ 2024-01-29T10:45:31Z
+
+
+ 2024-01-29T10:45:32Z
+
+
+ 2024-01-29T10:45:33Z
+
+
+ 2024-01-29T10:45:34Z
+
+
+ 2024-01-29T10:45:35Z
+
+
+ 2024-01-29T10:45:36Z
+
+
+ 2024-01-29T10:45:37Z
+
+
+ 2024-01-29T10:45:38Z
+
+
+ 2024-01-29T10:45:39Z
+
+
+ 2024-01-29T10:45:40Z
+
+
+ 2024-01-29T10:45:41Z
+
+
+ 2024-01-29T10:45:42Z
+
+
+ 2024-01-29T10:45:43Z
+
+
+ 2024-01-29T10:45:44Z
+
+
+ 2024-01-29T10:45:45Z
+
+
+ 2024-01-29T10:45:46Z
+
+
+ 2024-01-29T10:45:47Z
+
+
+ 2024-01-29T10:45:48Z
+
+
+ 2024-01-29T10:45:49Z
+
+
+ 2024-01-29T10:45:50Z
+
+
+ 2024-01-29T10:45:51Z
+
+
+ 2024-01-29T10:45:52Z
+
+
+ 2024-01-29T10:45:53Z
+
+
+ 2024-01-29T10:45:54Z
+
+
+ 2024-01-29T10:53:02Z
+
+
+ 2024-01-29T10:53:03Z
+
+
+ 2024-01-29T10:53:04Z
+
+
+ 2024-01-29T10:53:05Z
+
+
+ 2024-01-29T10:53:06Z
+
+
+ 2024-01-29T10:53:07Z
+
+
+ 2024-01-29T10:53:09Z
+
+
+ 2024-01-29T10:53:10Z
+
+
+ 2024-01-29T10:53:11Z
+
+
+ 2024-01-29T10:53:12Z
+
+
+ 2024-01-29T10:53:13Z
+
+
+ 2024-01-29T10:53:14Z
+
+
+ 2024-01-29T10:53:15Z
+
+
+ 2024-01-29T10:53:16Z
+
+
+ 2024-01-29T10:53:17Z
+
+
+ 2024-01-29T10:53:18Z
+
+
+ 2024-01-29T10:53:19Z
+
+
+ 2024-01-29T10:53:20Z
+
+
+ 2024-01-29T10:53:21Z
+
+
+ 2024-01-29T10:53:22Z
+
+
+ 2024-01-29T10:53:23Z
+
+
+ 2024-01-29T10:53:24Z
+
+
+ 2024-01-29T10:53:25Z
+
+
+ 2024-01-29T10:53:26Z
+
+
+ 2024-01-29T10:53:27Z
+
+
+ 2024-01-29T10:53:28Z
+
+
+ 2024-01-29T10:53:29Z
+
+
+ 2024-01-29T10:53:30Z
+
+
+ 2024-01-29T10:53:31Z
+
+
+ 2024-01-29T10:53:32Z
+
+
+ 2024-01-29T10:53:33Z
+
+
+ 2024-01-29T10:53:34Z
+
+
+ 2024-01-29T10:53:35Z
+
+
+ 2024-01-29T10:53:36Z
+
+
+ 2024-01-29T10:53:37Z
+
+
+ 2024-01-29T10:53:38Z
+
+
+ 2024-01-29T10:53:39Z
+
+
+ 2024-01-29T10:53:40Z
+
+
+ 2024-01-29T10:53:41Z
+
+
+ 2024-01-29T10:53:42Z
+
+
+ 2024-01-29T10:53:43Z
+
+
+ 2024-01-29T10:53:44Z
+
+
+ 2024-01-29T10:53:45Z
+
+
+ 2024-01-29T10:53:46Z
+
+
+ 2024-01-29T10:53:47Z
+
+
+ 2024-01-29T10:53:48Z
+
+
+ 2024-01-29T10:53:49Z
+
+
+ 2024-01-29T10:53:50Z
+
+
+ 2024-01-29T10:53:51Z
+
+
+ 2024-01-29T10:53:52Z
+
+
+ 2024-01-29T10:53:53Z
+
+
+ 2024-01-29T10:53:54Z
+
+
+ 2024-01-29T10:53:55Z
+
+
+ 2024-01-29T10:53:56Z
+
+
+ 2024-01-29T10:53:57Z
+
+
+ 2024-01-29T10:53:58Z
+
+
+ 2024-01-29T10:53:59Z
+
+
+ 2024-01-29T10:54:00Z
+
+
+ 2024-01-29T10:54:01Z
+
+
+ 2024-01-29T10:54:02Z
+
+
+ 2024-01-29T10:54:03Z
+
+
+ 2024-01-29T10:54:04Z
+
+
+ 2024-01-29T10:54:05Z
+
+
+ 2024-01-29T10:54:06Z
+
+
+ 2024-01-29T10:54:07Z
+
+
+ 2024-01-29T10:54:08Z
+
+
+ 2024-01-29T10:54:09Z
+
+
+ 2024-01-29T10:54:10Z
+
+
+ 2024-01-29T10:54:11Z
+
+
+ 2024-01-29T10:54:12Z
+
+
+ 2024-01-29T10:54:13Z
+
+
+ 2024-01-29T10:54:14Z
+
+
+ 2024-01-29T10:54:15Z
+
+
+ 2024-01-29T10:54:16Z
+
+
+ 2024-01-29T10:54:17Z
+
+
+ 2024-01-29T10:54:18Z
+
+
+ 2024-01-29T10:54:19Z
+
+
+ 2024-01-29T10:54:20Z
+
+
+ 2024-01-29T10:54:21Z
+
+
+ 2024-01-29T10:54:22Z
+
+
+ 2024-01-29T10:54:23Z
+
+
+ 2024-01-29T10:54:24Z
+
+
+ 2024-01-29T10:54:25Z
+
+
+ 2024-01-29T10:54:26Z
+
+
+ 2024-01-29T10:54:27Z
+
+
+ 2024-01-29T10:54:28Z
+
+
+ 2024-01-29T10:54:29Z
+
+
+ 2024-01-29T10:54:30Z
+
+
+ 2024-01-29T10:54:31Z
+
+
+ 2024-01-29T10:54:32Z
+
+
+ 2024-01-29T10:54:33Z
+
+
+ 2024-01-29T10:54:34Z
+
+
+ 2024-01-29T10:54:35Z
+
+
+ 2024-01-29T10:54:36Z
+
+
+ 2024-01-29T10:54:37Z
+
+
+ 2024-01-29T10:54:38Z
+
+
+ 2024-01-29T10:54:39Z
+
+
+ 2024-01-29T10:54:40Z
+
+
+ 2024-01-29T10:54:41Z
+
+
+ 2024-01-29T10:54:42Z
+
+
+ 2024-01-29T10:54:43Z
+
+
+ 2024-01-29T10:54:44Z
+
+
+ 2024-01-29T10:54:45Z
+
+
+ 2024-01-29T10:54:46Z
+
+
+ 2024-01-29T10:54:47Z
+
+
+ 2024-01-29T10:54:48Z
+
+
+ 2024-01-29T10:54:49Z
+
+
+ 2024-01-29T10:54:50Z
+
+
+ 2024-01-29T10:54:51Z
+
+
+ 2024-01-29T10:54:52Z
+
+
+ 2024-01-29T10:54:53Z
+
+
+ 2024-01-29T10:54:54Z
+
+
+ 2024-01-29T10:54:55Z
+
+
+ 2024-01-29T10:54:56Z
+
+
+ 2024-01-29T10:54:57Z
+
+
+ 2024-01-29T10:54:58Z
+
+
+ 2024-01-29T10:54:59Z
+
+
+ 2024-01-29T10:55:00Z
+
+
+ 2024-01-29T10:55:01Z
+
+
+ 2024-01-29T10:55:02Z
+
+
+ 2024-01-29T10:55:03Z
+
+
+ 2024-01-29T10:55:04Z
+
+
+ 2024-01-29T10:55:05Z
+
+
+ 2024-01-29T10:55:06Z
+
+
+ 2024-01-29T10:55:07Z
+
+
+ 2024-01-29T10:55:08Z
+
+
+ 2024-01-29T10:55:09Z
+
+
+ 2024-01-29T10:55:10Z
+
+
+ 2024-01-29T10:55:11Z
+
+
+ 2024-01-29T10:55:12Z
+
+
+ 2024-01-29T10:55:13Z
+
+
+ 2024-01-29T10:55:24Z
+
+
+ 2024-01-29T10:55:25Z
+
+
+ 2024-01-29T10:55:26Z
+
+
+ 2024-01-29T10:55:27Z
+
+
+ 2024-01-29T10:55:28Z
+
+
+ 2024-01-29T10:55:29Z
+
+
+ 2024-01-29T10:55:30Z
+
+
+ 2024-01-29T10:55:31Z
+
+
+ 2024-01-29T10:55:32Z
+
+
+ 2024-01-29T10:55:33Z
+
+
+ 2024-01-29T10:55:34Z
+
+
+ 2024-01-29T10:55:35Z
+
+
+ 2024-01-29T10:55:36Z
+
+
+ 2024-01-29T10:55:37Z
+
+
+ 2024-01-29T10:55:38Z
+
+
+ 2024-01-29T10:57:52Z
+
+
+ 2024-01-29T10:57:53Z
+
+
+ 2024-01-29T10:57:54Z
+
+
+ 2024-01-29T10:57:55Z
+
+
+ 2024-01-29T10:58:01Z
+
+
+ 2024-01-29T10:58:02Z
+
+
+ 2024-01-29T10:58:03Z
+
+
+ 2024-01-29T10:58:04Z
+
+
+ 2024-01-29T10:58:05Z
+
+
+ 2024-01-29T10:58:06Z
+
+
+ 2024-01-29T10:58:07Z
+
+
+ 2024-01-29T10:58:08Z
+
+
+ 2024-01-29T10:58:09Z
+
+
+ 2024-01-29T10:58:10Z
+
+
+ 2024-01-29T10:58:11Z
+
+
+ 2024-01-29T10:58:12Z
+
+
+ 2024-01-29T10:58:13Z
+
+
+ 2024-01-29T10:58:14Z
+
+
+ 2024-01-29T10:58:15Z
+
+
+ 2024-01-29T10:58:16Z
+
+
+ 2024-01-29T10:58:17Z
+
+
+ 2024-01-29T10:58:18Z
+
+
+ 2024-01-29T10:58:19Z
+
+
+ 2024-01-29T10:58:20Z
+
+
+ 2024-01-29T10:58:21Z
+
+
+ 2024-01-29T10:58:22Z
+
+
+ 2024-01-29T10:58:23Z
+
+
+ 2024-01-29T10:58:24Z
+
+
+ 2024-01-29T10:58:25Z
+
+
+ 2024-01-29T10:58:26Z
+
+
+ 2024-01-29T10:58:27Z
+
+
+ 2024-01-29T10:58:28Z
+
+
+ 2024-01-29T10:58:29Z
+
+
+ 2024-01-29T10:58:30Z
+
+
+ 2024-01-29T10:58:31Z
+
+
+ 2024-01-29T10:58:32Z
+
+
+ 2024-01-29T10:58:33Z
+
+
+ 2024-01-29T10:58:34Z
+
+
+ 2024-01-29T10:58:35Z
+
+
+ 2024-01-29T10:58:36Z
+
+
+ 2024-01-29T10:58:37Z
+
+
+ 2024-01-29T10:58:38Z
+
+
+ 2024-01-29T10:58:39Z
+
+
+ 2024-01-29T10:58:40Z
+
+
+ 2024-01-29T10:58:41Z
+
+
+ 2024-01-29T10:58:42Z
+
+
+ 2024-01-29T10:58:43Z
+
+
+ 2024-01-29T10:58:44Z
+
+
+ 2024-01-29T10:58:45Z
+
+
+ 2024-01-29T10:58:46Z
+
+
+ 2024-01-29T10:58:47Z
+
+
+ 2024-01-29T10:58:48Z
+
+
+ 2024-01-29T10:58:49Z
+
+
+ 2024-01-29T10:58:50Z
+
+
+ 2024-01-29T10:58:51Z
+
+
+ 2024-01-29T10:58:52Z
+
+
+ 2024-01-29T10:58:53Z
+
+
+ 2024-01-29T10:58:54Z
+
+
+ 2024-01-29T10:58:55Z
+
+
+ 2024-01-29T10:58:56Z
+
+
+ 2024-01-29T10:58:57Z
+
+
+ 2024-01-29T10:58:58Z
+
+
+ 2024-01-29T10:58:59Z
+
+
+ 2024-01-29T10:59:00Z
+
+
+ 2024-01-29T10:59:01Z
+
+
+ 2024-01-29T10:59:02Z
+
+
+ 2024-01-29T10:59:03Z
+
+
+ 2024-01-29T10:59:04Z
+
+
+ 2024-01-29T10:59:05Z
+
+
+ 2024-01-29T10:59:06Z
+
+
+ 2024-01-29T10:59:07Z
+
+
+ 2024-01-29T10:59:08Z
+
+
+ 2024-01-29T10:59:09Z
+
+
+ 2024-01-29T10:59:10Z
+
+
+ 2024-01-29T10:59:11Z
+
+
+ 2024-01-29T10:59:12Z
+
+
+ 2024-01-29T10:59:13Z
+
+
+ 2024-01-29T10:59:14Z
+
+
+ 2024-01-29T10:59:15Z
+
+
+ 2024-01-29T10:59:16Z
+
+
+ 2024-01-29T10:59:17Z
+
+
+ 2024-01-29T10:59:18Z
+
+
+ 2024-01-29T10:59:19Z
+
+
+ 2024-01-29T10:59:20Z
+
+
+ 2024-01-29T10:59:21Z
+
+
+ 2024-01-29T10:59:22Z
+
+
+ 2024-01-29T10:59:23Z
+
+
+ 2024-01-29T10:59:24Z
+
+
+ 2024-01-29T10:59:25Z
+
+
+ 2024-01-29T10:59:26Z
+
+
+ 2024-01-29T10:59:27Z
+
+
+ 2024-01-29T10:59:28Z
+
+
+ 2024-01-29T10:59:29Z
+
+
+ 2024-01-29T10:59:30Z
+
+
+ 2024-01-29T10:59:31Z
+
+
+ 2024-01-29T10:59:32Z
+
+
+ 2024-01-29T10:59:33Z
+
+
+ 2024-01-29T10:59:34Z
+
+
+ 2024-01-29T10:59:35Z
+
+
+ 2024-01-29T10:59:36Z
+
+
+ 2024-01-29T10:59:37Z
+
+
+ 2024-01-29T10:59:38Z
+
+
+ 2024-01-29T10:59:39Z
+
+
+ 2024-01-29T10:59:40Z
+
+
+ 2024-01-29T10:59:41Z
+
+
+ 2024-01-29T10:59:42Z
+
+
+ 2024-01-29T10:59:43Z
+
+
+ 2024-01-29T10:59:44Z
+
+
+ 2024-01-29T10:59:45Z
+
+
+ 2024-01-29T10:59:46Z
+
+
+ 2024-01-29T10:59:47Z
+
+
+ 2024-01-29T10:59:48Z
+
+
+ 2024-01-29T10:59:49Z
+
+
+ 2024-01-29T10:59:50Z
+
+
+ 2024-01-29T10:59:51Z
+
+
+ 2024-01-29T10:59:52Z
+
+
+ 2024-01-29T10:59:53Z
+
+
+ 2024-01-29T10:59:54Z
+
+
+ 2024-01-29T10:59:55Z
+
+
+ 2024-01-29T10:59:56Z
+
+
+ 2024-01-29T10:59:57Z
+
+
+ 2024-01-29T10:59:58Z
+
+
+ 2024-01-29T10:59:59Z
+
+
+ 2024-01-29T11:00:00Z
+
+
+ 2024-01-29T11:00:01Z
+
+
+ 2024-01-29T11:00:02Z
+
+
+ 2024-01-29T11:00:03Z
+
+
+ 2024-01-29T11:00:04Z
+
+
+ 2024-01-29T11:00:05Z
+
+
+ 2024-01-29T11:00:06Z
+
+
+ 2024-01-29T11:00:07Z
+
+
+ 2024-01-29T11:00:08Z
+
+
+ 2024-01-29T11:00:09Z
+
+
+ 2024-01-29T11:00:10Z
+
+
+ 2024-01-29T11:00:11Z
+
+
+ 2024-01-29T11:00:12Z
+
+
+ 2024-01-29T11:00:13Z
+
+
+ 2024-01-29T11:00:14Z
+
+
+ 2024-01-29T11:00:15Z
+
+
+ 2024-01-29T11:00:16Z
+
+
+ 2024-01-29T11:00:17Z
+
+
+ 2024-01-29T11:00:18Z
+
+
+ 2024-01-29T11:00:19Z
+
+
+ 2024-01-29T11:00:20Z
+
+
+ 2024-01-29T11:00:21Z
+
+
+ 2024-01-29T11:00:22Z
+
+
+ 2024-01-29T11:00:23Z
+
+
+ 2024-01-29T11:00:24Z
+
+
+ 2024-01-29T11:00:25Z
+
+
+ 2024-01-29T11:00:26Z
+
+
+ 2024-01-29T11:00:27Z
+
+
+ 2024-01-29T11:00:28Z
+
+
+ 2024-01-29T11:00:29Z
+
+
+ 2024-01-29T11:00:30Z
+
+
+ 2024-01-29T11:00:31Z
+
+
+ 2024-01-29T11:00:32Z
+
+
+ 2024-01-29T11:00:33Z
+
+
+ 2024-01-29T11:00:34Z
+
+
+ 2024-01-29T11:00:35Z
+
+
+ 2024-01-29T11:00:36Z
+
+
+ 2024-01-29T11:00:37Z
+
+
+ 2024-01-29T11:00:38Z
+
+
+ 2024-01-29T11:00:39Z
+
+
+ 2024-01-29T11:00:40Z
+
+
+ 2024-01-29T11:00:41Z
+
+
+ 2024-01-29T11:00:42Z
+
+
+ 2024-01-29T11:00:43Z
+
+
+ 2024-01-29T11:00:44Z
+
+
+ 2024-01-29T11:00:45Z
+
+
+ 2024-01-29T11:00:46Z
+
+
+ 2024-01-29T11:00:47Z
+
+
+ 2024-01-29T11:00:48Z
+
+
+ 2024-01-29T11:00:49Z
+
+
+ 2024-01-29T11:00:50Z
+
+
+ 2024-01-29T11:00:51Z
+
+
+ 2024-01-29T11:00:52Z
+
+
+ 2024-01-29T11:00:53Z
+
+
+ 2024-01-29T11:00:54Z
+
+
+ 2024-01-29T11:00:55Z
+
+
+ 2024-01-29T11:00:56Z
+
+
+ 2024-01-29T11:00:57Z
+
+
+ 2024-01-29T11:00:58Z
+
+
+ 2024-01-29T11:00:59Z
+
+
+ 2024-01-29T11:01:00Z
+
+
+ 2024-01-29T11:01:01Z
+
+
+ 2024-01-29T11:01:02Z
+
+
+ 2024-01-29T11:01:03Z
+
+
+ 2024-01-29T11:01:04Z
+
+
+ 2024-01-29T11:01:05Z
+
+
+ 2024-01-29T11:01:06Z
+
+
+ 2024-01-29T11:01:07Z
+
+
+ 2024-01-29T11:01:08Z
+
+
+ 2024-01-29T11:01:09Z
+
+
+ 2024-01-29T11:01:10Z
+
+
+ 2024-01-29T11:01:11Z
+
+
+ 2024-01-29T11:01:12Z
+
+
+ 2024-01-29T11:01:13Z
+
+
+ 2024-01-29T11:01:14Z
+
+
+ 2024-01-29T11:01:15Z
+
+
+ 2024-01-29T11:01:16Z
+
+
+ 2024-01-29T11:01:17Z
+
+
+ 2024-01-29T11:01:18Z
+
+
+ 2024-01-29T11:01:19Z
+
+
+ 2024-01-29T11:01:20Z
+
+
+ 2024-01-29T11:01:21Z
+
+
+ 2024-01-29T11:01:22Z
+
+
+ 2024-01-29T11:01:23Z
+
+
+ 2024-01-29T11:01:24Z
+
+
+ 2024-01-29T11:01:25Z
+
+
+ 2024-01-29T11:01:26Z
+
+
+ 2024-01-29T11:01:27Z
+
+
+ 2024-01-29T11:01:28Z
+
+
+ 2024-01-29T11:01:29Z
+
+
+ 2024-01-29T11:01:30Z
+
+
+ 2024-01-29T11:01:31Z
+
+
+ 2024-01-29T11:01:32Z
+
+
+ 2024-01-29T11:01:33Z
+
+
+ 2024-01-29T11:01:34Z
+
+
+ 2024-01-29T11:01:35Z
+
+
+ 2024-01-29T11:01:36Z
+
+
+ 2024-01-29T11:01:37Z
+
+
+ 2024-01-29T11:01:38Z
+
+
+ 2024-01-29T11:01:39Z
+
+
+ 2024-01-29T11:01:40Z
+
+
+ 2024-01-29T11:01:41Z
+
+
+ 2024-01-29T11:01:42Z
+
+
+ 2024-01-29T11:01:43Z
+
+
+ 2024-01-29T11:01:44Z
+
+
+ 2024-01-29T11:01:45Z
+
+
+ 2024-01-29T11:01:46Z
+
+
+ 2024-01-29T11:01:47Z
+
+
+ 2024-01-29T11:01:48Z
+
+
+ 2024-01-29T11:01:49Z
+
+
+ 2024-01-29T11:01:50Z
+
+
+ 2024-01-29T11:01:51Z
+
+
+ 2024-01-29T11:01:52Z
+
+
+ 2024-01-29T13:10:50Z
+
+
+ 2024-01-29T13:10:51Z
+
+
+ 2024-01-29T13:10:52Z
+
+
+ 2024-01-29T13:10:53Z
+
+
+ 2024-01-29T13:10:54Z
+
+
+ 2024-01-29T13:10:55Z
+
+
+ 2024-01-29T13:10:56Z
+
+
+ 2024-01-29T13:10:57Z
+
+
+ 2024-01-29T13:10:58Z
+
+
+ 2024-01-29T13:10:59Z
+
+
+ 2024-01-29T13:11:00Z
+
+
+ 2024-01-29T13:11:01Z
+
+
+ 2024-01-29T13:11:02Z
+
+
+ 2024-01-29T13:11:03Z
+
+
+ 2024-01-29T13:11:04Z
+
+
+ 2024-01-29T13:11:05Z
+
+
+ 2024-01-29T13:11:06Z
+
+
+ 2024-01-29T13:11:07Z
+
+
+ 2024-01-29T13:11:08Z
+
+
+ 2024-01-29T13:11:09Z
+
+
+ 2024-01-29T13:11:10Z
+
+
+ 2024-01-29T13:11:11Z
+
+
+ 2024-01-29T13:11:12Z
+
+
+ 2024-01-29T13:11:13Z
+
+
+ 2024-01-29T13:11:14Z
+
+
+ 2024-01-29T13:11:15Z
+
+
+ 2024-01-29T13:11:16Z
+
+
+ 2024-01-29T13:11:17Z
+
+
+ 2024-01-29T13:11:18Z
+
+
+ 2024-01-29T13:11:19Z
+
+
+ 2024-01-29T13:11:20Z
+
+
+ 2024-01-29T13:11:21Z
+
+
+ 2024-01-29T13:11:22Z
+
+
+ 2024-01-29T13:11:23Z
+
+
+ 2024-01-29T13:11:24Z
+
+
+ 2024-01-29T13:11:25Z
+
+
+ 2024-01-29T13:11:26Z
+
+
+ 2024-01-29T13:11:27Z
+
+
+ 2024-01-29T13:11:28Z
+
+
+ 2024-01-29T13:11:29Z
+
+
+ 2024-01-29T13:11:30Z
+
+
+ 2024-01-29T13:11:31Z
+
+
+ 2024-01-29T13:11:32Z
+
+
+ 2024-01-29T13:11:33Z
+
+
+ 2024-01-29T13:11:34Z
+
+
+ 2024-01-29T13:11:35Z
+
+
+ 2024-01-29T13:11:36Z
+
+
+ 2024-01-29T13:11:37Z
+
+
+ 2024-01-29T13:11:38Z
+
+
+ 2024-01-29T13:11:39Z
+
+
+ 2024-01-29T13:11:40Z
+
+
+ 2024-01-29T13:11:41Z
+
+
+ 2024-01-29T13:11:42Z
+
+
+ 2024-01-29T13:11:43Z
+
+
+ 2024-01-29T13:11:44Z
+
+
+ 2024-01-29T13:11:45Z
+
+
+ 2024-01-29T13:11:46Z
+
+
+ 2024-01-29T13:11:47Z
+
+
+ 2024-01-29T13:11:48Z
+
+
+ 2024-01-29T13:11:49Z
+
+
+ 2024-01-29T13:11:50Z
+
+
+ 2024-01-29T13:11:51Z
+
+
+ 2024-01-29T13:11:52Z
+
+
+ 2024-01-29T13:11:53Z
+
+
+ 2024-01-29T13:11:54Z
+
+
+ 2024-01-29T13:11:55Z
+
+
+ 2024-01-29T13:11:56Z
+
+
+ 2024-01-29T13:11:57Z
+
+
+ 2024-01-29T13:11:58Z
+
+
+ 2024-01-29T13:11:59Z
+
+
+ 2024-01-29T13:12:00Z
+
+
+ 2024-01-29T13:12:01Z
+
+
+ 2024-01-29T13:12:02Z
+
+
+ 2024-01-29T13:12:03Z
+
+
+ 2024-01-29T13:12:04Z
+
+
+ 2024-01-29T13:12:05Z
+
+
+ 2024-01-29T13:12:06Z
+
+
+ 2024-01-29T13:12:07Z
+
+
+ 2024-01-29T13:12:08Z
+
+
+ 2024-01-29T13:12:09Z
+
+
+ 2024-01-29T13:12:10Z
+
+
+ 2024-01-29T13:12:11Z
+
+
+ 2024-01-29T13:12:12Z
+
+
+ 2024-01-29T13:12:13Z
+
+
+ 2024-01-29T13:12:14Z
+
+
+ 2024-01-29T13:12:15Z
+
+
+ 2024-01-29T13:12:16Z
+
+
+ 2024-01-29T13:12:17Z
+
+
+ 2024-01-29T13:12:18Z
+
+
+ 2024-01-29T13:12:19Z
+
+
+ 2024-01-29T13:12:20Z
+
+
+ 2024-01-29T13:12:21Z
+
+
+ 2024-01-29T13:12:22Z
+
+
+ 2024-01-29T13:12:23Z
+
+
+ 2024-01-29T13:12:24Z
+
+
+ 2024-01-29T13:12:25Z
+
+
+ 2024-01-29T13:12:26Z
+
+
+ 2024-01-29T13:12:27Z
+
+
+ 2024-01-29T13:12:28Z
+
+
+ 2024-01-29T13:12:29Z
+
+
+ 2024-01-29T13:12:30Z
+
+
+ 2024-01-29T13:12:31Z
+
+
+ 2024-01-29T13:12:32Z
+
+
+ 2024-01-29T13:12:33Z
+
+
+ 2024-01-29T13:12:34Z
+
+
+ 2024-01-29T13:12:35Z
+
+
+ 2024-01-29T13:12:36Z
+
+
+ 2024-01-29T13:12:37Z
+
+
+ 2024-01-29T13:12:38Z
+
+
+ 2024-01-29T13:12:39Z
+
+
+ 2024-01-29T13:12:40Z
+
+
+ 2024-01-29T13:12:41Z
+
+
+ 2024-01-29T13:12:42Z
+
+
+ 2024-01-29T13:12:43Z
+
+
+ 2024-01-29T13:12:44Z
+
+
+ 2024-01-29T13:12:45Z
+
+
+ 2024-01-29T13:12:46Z
+
+
+ 2024-01-29T13:12:47Z
+
+
+ 2024-01-29T13:12:48Z
+
+
+ 2024-01-29T13:12:49Z
+
+
+ 2024-01-29T13:12:50Z
+
+
+ 2024-01-29T13:12:51Z
+
+
+ 2024-01-29T13:12:52Z
+
+
+ 2024-01-29T13:12:53Z
+
+
+ 2024-01-29T13:12:54Z
+
+
+ 2024-01-29T13:12:56Z
+
+
+ 2024-01-29T13:12:57Z
+
+
+ 2024-01-29T13:12:58Z
+
+
+ 2024-01-29T13:12:59Z
+
+
+ 2024-01-29T13:13:00Z
+
+
+ 2024-01-29T13:13:01Z
+
+
+ 2024-01-29T13:13:02Z
+
+
+ 2024-01-29T13:13:03Z
+
+
+ 2024-01-29T13:13:04Z
+
+
+ 2024-01-29T13:13:05Z
+
+
+ 2024-01-29T13:13:06Z
+
+
+ 2024-01-29T13:13:07Z
+
+
+ 2024-01-29T13:13:08Z
+
+
+ 2024-01-29T13:13:09Z
+
+
+ 2024-01-29T13:13:10Z
+
+
+ 2024-01-29T13:13:11Z
+
+
+ 2024-01-29T13:13:12Z
+
+
+ 2024-01-29T13:13:13Z
+
+
+ 2024-01-29T13:13:14Z
+
+
+ 2024-01-29T13:13:15Z
+
+
+ 2024-01-29T13:13:16Z
+
+
+ 2024-01-29T13:13:17Z
+
+
+ 2024-01-29T13:13:18Z
+
+
+ 2024-01-29T13:13:19Z
+
+
+ 2024-01-29T13:13:20Z
+
+
+ 2024-01-29T13:13:21Z
+
+
+ 2024-01-29T13:13:22Z
+
+
+ 2024-01-29T13:13:23Z
+
+
+ 2024-01-29T13:13:24Z
+
+
+ 2024-01-29T13:13:25Z
+
+
+ 2024-01-29T13:13:26Z
+
+
+ 2024-01-29T13:13:27Z
+
+
+ 2024-01-29T13:13:28Z
+
+
+ 2024-01-29T13:13:29Z
+
+
+ 2024-01-29T13:13:30Z
+
+
+ 2024-01-29T13:13:31Z
+
+
+ 2024-01-29T13:13:32Z
+
+
+ 2024-01-29T13:13:33Z
+
+
+ 2024-01-29T13:13:34Z
+
+
+ 2024-01-29T13:13:35Z
+
+
+ 2024-01-29T13:13:36Z
+
+
+ 2024-01-29T13:13:37Z
+
+
+ 2024-01-29T13:13:38Z
+
+
+ 2024-01-29T13:13:39Z
+
+
+ 2024-01-29T13:13:40Z
+
+
+ 2024-01-29T13:13:41Z
+
+
+ 2024-01-29T13:13:42Z
+
+
+ 2024-01-29T13:13:43Z
+
+
+ 2024-01-29T13:13:44Z
+
+
+ 2024-01-29T13:13:45Z
+
+
+ 2024-01-29T13:13:46Z
+
+
+ 2024-01-29T13:13:47Z
+
+
+ 2024-01-29T13:13:48Z
+
+
+ 2024-01-29T13:13:49Z
+
+
+ 2024-01-29T13:13:50Z
+
+
+ 2024-01-29T13:13:51Z
+
+
+ 2024-01-29T13:13:52Z
+
+
+ 2024-01-29T13:13:53Z
+
+
+ 2024-01-29T13:13:54Z
+
+
+ 2024-01-29T13:13:55Z
+
+
+ 2024-01-29T13:13:56Z
+
+
+ 2024-01-29T13:13:57Z
+
+
+ 2024-01-29T13:13:58Z
+
+
+ 2024-01-29T13:13:59Z
+
+
+ 2024-01-29T13:14:00Z
+
+
+ 2024-01-29T13:14:01Z
+
+
+ 2024-01-29T13:14:02Z
+
+
+ 2024-01-29T13:14:03Z
+
+
+ 2024-01-29T13:14:04Z
+
+
+ 2024-01-29T13:14:05Z
+
+
+ 2024-01-29T13:14:06Z
+
+
+ 2024-01-29T13:14:07Z
+
+
+ 2024-01-29T13:14:08Z
+
+
+ 2024-01-29T13:14:09Z
+
+
+ 2024-01-29T13:14:10Z
+
+
+ 2024-01-29T13:14:11Z
+
+
+ 2024-01-29T13:14:12Z
+
+
+ 2024-01-29T13:14:13Z
+
+
+ 2024-01-29T13:14:14Z
+
+
+ 2024-01-29T13:14:15Z
+
+
+ 2024-01-29T13:14:29Z
+
+
+ 2024-01-29T13:14:30Z
+
+
+ 2024-01-29T13:14:31Z
+
+
+ 2024-01-29T13:14:32Z
+
+
+ 2024-01-29T13:15:52Z
+
+
+ 2024-01-29T13:15:53Z
+
+
+ 2024-01-29T13:15:54Z
+
+
+ 2024-01-29T13:15:55Z
+
+
+ 2024-01-29T13:15:56Z
+
+
+ 2024-01-29T13:15:57Z
+
+
+ 2024-01-29T13:15:58Z
+
+
+ 2024-01-29T13:15:59Z
+
+
+ 2024-01-29T13:16:00Z
+
+
+ 2024-01-29T13:16:01Z
+
+
+ 2024-01-29T13:16:02Z
+
+
+ 2024-01-29T13:16:03Z
+
+
+ 2024-01-29T13:16:04Z
+
+
+ 2024-01-29T13:16:05Z
+
+
+ 2024-01-29T13:16:06Z
+
+
+ 2024-01-29T13:16:07Z
+
+
+ 2024-01-29T13:16:08Z
+
+
+ 2024-01-29T13:16:09Z
+
+
+ 2024-01-29T13:16:10Z
+
+
+ 2024-01-29T13:16:11Z
+
+
+ 2024-01-29T13:16:12Z
+
+
+ 2024-01-29T13:16:13Z
+
+
+ 2024-01-29T13:16:14Z
+
+
+ 2024-01-29T13:16:15Z
+
+
+ 2024-01-29T13:16:16Z
+
+
+ 2024-01-29T13:16:17Z
+
+
+ 2024-01-29T13:16:18Z
+
+
+ 2024-01-29T13:16:19Z
+
+
+ 2024-01-29T13:16:20Z
+
+
+ 2024-01-29T13:16:21Z
+
+
+ 2024-01-29T13:16:22Z
+
+
+ 2024-01-29T13:16:23Z
+
+
+ 2024-01-29T13:16:24Z
+
+
+ 2024-01-29T13:16:25Z
+
+
+ 2024-01-29T13:16:26Z
+
+
+ 2024-01-29T13:16:27Z
+
+
+ 2024-01-29T13:16:28Z
+
+
+ 2024-01-29T13:16:29Z
+
+
+ 2024-01-29T13:16:30Z
+
+
+ 2024-01-29T13:16:31Z
+
+
+ 2024-01-29T13:16:32Z
+
+
+ 2024-01-29T13:16:33Z
+
+
+ 2024-01-29T13:16:34Z
+
+
+ 2024-01-29T13:16:35Z
+
+
+ 2024-01-29T13:16:36Z
+
+
+
+
+ 20231127180206.gpx
+ Daily routes
+ /user/AmOosm/traces/11276808
+
+
+ 2023-11-27T17:06:42Z
+
+
+ 2023-11-27T17:06:43Z
+
+
+ 2023-11-27T17:06:44Z
+
+
+ 2023-11-27T17:06:45Z
+
+
+ 2023-11-27T17:06:46Z
+
+
+ 2023-11-27T17:06:47Z
+
+
+ 2023-11-27T17:06:48Z
+
+
+ 2023-11-27T17:06:49Z
+
+
+ 2023-11-27T17:06:50Z
+
+
+ 2023-11-27T17:06:51Z
+
+
+ 2023-11-27T17:06:52Z
+
+
+ 2023-11-27T17:06:53Z
+
+
+ 2023-11-27T17:06:54Z
+
+
+ 2023-11-27T17:06:55Z
+
+
+ 2023-11-27T17:06:56Z
+
+
+ 2023-11-27T17:06:57Z
+
+
+ 2023-11-27T17:06:58Z
+
+
+ 2023-11-27T17:06:59Z
+
+
+ 2023-11-27T17:07:00Z
+
+
+ 2023-11-27T17:07:01Z
+
+
+ 2023-11-27T17:07:02Z
+
+
+ 2023-11-27T17:07:03Z
+
+
+ 2023-11-27T17:07:04Z
+
+
+ 2023-11-27T17:07:05Z
+
+
+ 2023-11-27T17:07:06Z
+
+
+ 2023-11-27T17:07:07Z
+
+
+ 2023-11-27T17:07:08Z
+
+
+ 2023-11-27T17:07:09Z
+
+
+ 2023-11-27T17:07:10Z
+
+
+ 2023-11-27T17:07:11Z
+
+
+ 2023-11-27T17:07:12Z
+
+
+ 2023-11-27T17:07:13Z
+
+
+ 2023-11-27T17:07:14Z
+
+
+ 2023-11-27T17:07:15Z
+
+
+ 2023-11-27T17:07:16Z
+
+
+ 2023-11-27T17:07:17Z
+
+
+ 2023-11-27T17:07:18Z
+
+
+ 2023-11-27T17:07:19Z
+
+
+ 2023-11-27T17:07:20Z
+
+
+ 2023-11-27T17:07:21Z
+
+
+ 2023-11-27T17:07:22Z
+
+
+ 2023-11-27T17:07:23Z
+
+
+ 2023-11-27T17:07:24Z
+
+
+ 2023-11-27T17:07:25Z
+
+
+ 2023-11-27T17:07:26Z
+
+
+ 2023-11-27T17:07:27Z
+
+
+ 2023-11-27T17:07:28Z
+
+
+ 2023-11-27T17:07:29Z
+
+
+ 2023-11-27T17:07:30Z
+
+
+ 2023-11-27T17:07:31Z
+
+
+ 2023-11-27T17:07:32Z
+
+
+ 2023-11-27T17:07:33Z
+
+
+ 2023-11-27T17:07:34Z
+
+
+ 2023-11-27T17:07:35Z
+
+
+ 2023-11-27T17:07:36Z
+
+
+ 2023-11-27T17:07:37Z
+
+
+ 2023-11-27T17:07:38Z
+
+
+ 2023-11-27T17:07:39Z
+
+
+ 2023-11-27T17:07:40Z
+
+
+ 2023-11-27T17:07:41Z
+
+
+ 2023-11-27T17:07:42Z
+
+
+ 2023-11-27T17:07:43Z
+
+
+ 2023-11-27T17:07:44Z
+
+
+ 2023-11-27T17:07:45Z
+
+
+ 2023-11-27T17:07:46Z
+
+
+ 2023-11-27T17:07:47Z
+
+
+ 2023-11-27T17:07:48Z
+
+
+ 2023-11-27T17:07:49Z
+
+
+ 2023-11-27T17:07:50Z
+
+
+ 2023-11-27T17:07:51Z
+
+
+ 2023-11-27T17:07:52Z
+
+
+ 2023-11-27T17:07:53Z
+
+
+ 2023-11-27T17:07:54Z
+
+
+ 2023-11-27T17:07:55Z
+
+
+ 2023-11-27T17:07:56Z
+
+
+ 2023-11-27T17:07:57Z
+
+
+ 2023-11-27T17:07:58Z
+
+
+ 2023-11-27T17:07:59Z
+
+
+ 2023-11-27T17:08:00Z
+
+
+ 2023-11-27T17:08:01Z
+
+
+ 2023-11-27T17:08:02Z
+
+
+ 2023-11-27T17:08:03Z
+
+
+ 2023-11-27T17:08:04Z
+
+
+ 2023-11-27T17:08:05Z
+
+
+ 2023-11-27T17:08:06Z
+
+
+ 2023-11-27T17:08:07Z
+
+
+ 2023-11-27T17:08:08Z
+
+
+ 2023-11-27T17:08:09Z
+
+
+ 2023-11-27T17:08:10Z
+
+
+ 2023-11-27T17:08:11Z
+
+
+ 2023-11-27T17:08:12Z
+
+
+ 2023-11-27T17:08:13Z
+
+
+ 2023-11-27T17:08:14Z
+
+
+ 2023-11-27T17:08:15Z
+
+
+ 2023-11-27T17:08:16Z
+
+
+ 2023-11-27T17:08:17Z
+
+
+ 2023-11-27T17:08:18Z
+
+
+ 2023-11-27T17:08:19Z
+
+
+ 2023-11-27T17:08:20Z
+
+
+ 2023-11-27T17:08:21Z
+
+
+ 2023-11-27T17:08:22Z
+
+
+ 2023-11-27T17:08:23Z
+
+
+ 2023-11-27T17:08:24Z
+
+
+ 2023-11-27T17:08:25Z
+
+
+ 2023-11-27T17:08:26Z
+
+
+ 2023-11-27T17:08:27Z
+
+
+ 2023-11-27T17:08:28Z
+
+
+ 2023-11-27T17:08:29Z
+
+
+ 2023-11-27T17:08:30Z
+
+
+ 2023-11-27T17:08:31Z
+
+
+ 2023-11-27T17:08:32Z
+
+
+ 2023-11-27T17:08:33Z
+
+
+ 2023-11-27T17:08:34Z
+
+
+ 2023-11-27T17:08:35Z
+
+
+ 2023-11-27T17:08:36Z
+
+
+ 2023-11-27T17:08:37Z
+
+
+ 2023-11-27T17:08:38Z
+
+
+ 2023-11-27T17:08:39Z
+
+
+ 2023-11-27T17:08:40Z
+
+
+ 2023-11-27T17:08:41Z
+
+
+ 2023-11-27T17:08:42Z
+
+
+ 2023-11-27T17:08:43Z
+
+
+ 2023-11-27T17:08:44Z
+
+
+ 2023-11-27T17:08:45Z
+
+
+ 2023-11-27T17:08:46Z
+
+
+ 2023-11-27T17:08:47Z
+
+
+ 2023-11-27T17:08:48Z
+
+
+ 2023-11-27T17:08:49Z
+
+
+ 2023-11-27T17:08:50Z
+
+
+ 2023-11-27T17:08:51Z
+
+
+ 2023-11-27T17:08:52Z
+
+
+ 2023-11-27T17:08:53Z
+
+
+ 2023-11-27T17:08:54Z
+
+
+ 2023-11-27T17:08:55Z
+
+
+ 2023-11-27T17:08:56Z
+
+
+ 2023-11-27T17:08:57Z
+
+
+ 2023-11-27T17:08:58Z
+
+
+ 2023-11-27T17:08:59Z
+
+
+ 2023-11-27T17:09:00Z
+
+
+ 2023-11-27T17:09:01Z
+
+
+ 2023-11-27T17:09:02Z
+
+
+ 2023-11-27T17:09:03Z
+
+
+ 2023-11-27T17:09:04Z
+
+
+ 2023-11-27T17:09:05Z
+
+
+ 2023-11-27T17:09:06Z
+
+
+ 2023-11-27T17:09:07Z
+
+
+ 2023-11-27T17:09:08Z
+
+
+ 2023-11-27T17:09:09Z
+
+
+ 2023-11-27T17:09:10Z
+
+
+ 2023-11-27T17:09:11Z
+
+
+ 2023-11-27T17:09:12Z
+
+
+ 2023-11-27T17:09:13Z
+
+
+ 2023-11-27T17:09:14Z
+
+
+ 2023-11-27T17:09:15Z
+
+
+ 2023-11-27T17:09:16Z
+
+
+ 2023-11-27T17:09:17Z
+
+
+ 2023-11-27T17:09:18Z
+
+
+ 2023-11-27T17:09:19Z
+
+
+ 2023-11-27T17:09:20Z
+
+
+ 2023-11-27T17:09:21Z
+
+
+ 2023-11-27T17:09:22Z
+
+
+ 2023-11-27T17:09:23Z
+
+
+ 2023-11-27T17:09:24Z
+
+
+ 2023-11-27T17:09:25Z
+
+
+ 2023-11-27T17:09:26Z
+
+
+ 2023-11-27T17:09:27Z
+
+
+ 2023-11-27T17:09:28Z
+
+
+ 2023-11-27T17:09:29Z
+
+
+ 2023-11-27T17:09:30Z
+
+
+ 2023-11-27T17:09:31Z
+
+
+ 2023-11-27T17:09:32Z
+
+
+ 2023-11-27T17:09:33Z
+
+
+ 2023-11-27T17:09:34Z
+
+
+ 2023-11-27T17:09:35Z
+
+
+ 2023-11-27T17:09:36Z
+
+
+ 2023-11-27T17:09:37Z
+
+
+ 2023-11-27T17:09:38Z
+
+
+ 2023-11-27T17:09:39Z
+
+
+ 2023-11-27T17:09:40Z
+
+
+ 2023-11-27T17:09:41Z
+
+
+ 2023-11-27T17:09:42Z
+
+
+ 2023-11-27T17:09:43Z
+
+
+ 2023-11-27T17:09:44Z
+
+
+ 2023-11-27T17:09:45Z
+
+
+ 2023-11-27T17:09:46Z
+
+
+ 2023-11-27T17:09:47Z
+
+
+ 2023-11-27T17:09:48Z
+
+
+ 2023-11-27T17:09:49Z
+
+
+ 2023-11-27T17:09:50Z
+
+
+ 2023-11-27T17:09:51Z
+
+
+ 2023-11-27T17:09:52Z
+
+
+ 2023-11-27T17:09:53Z
+
+
+ 2023-11-27T17:09:54Z
+
+
+ 2023-11-27T17:09:55Z
+
+
+ 2023-11-27T17:09:56Z
+
+
+ 2023-11-27T17:09:57Z
+
+
+ 2023-11-27T17:09:58Z
+
+
+ 2023-11-27T17:09:59Z
+
+
+ 2023-11-27T17:10:00Z
+
+
+ 2023-11-27T17:10:01Z
+
+
+ 2023-11-27T17:10:02Z
+
+
+ 2023-11-27T17:10:03Z
+
+
+ 2023-11-27T17:10:04Z
+
+
+ 2023-11-27T17:10:05Z
+
+
+ 2023-11-27T17:10:06Z
+
+
+ 2023-11-27T17:10:07Z
+
+
+ 2023-11-27T17:10:08Z
+
+
+ 2023-11-27T17:10:09Z
+
+
+ 2023-11-27T17:10:10Z
+
+
+ 2023-11-27T17:10:11Z
+
+
+ 2023-11-27T17:10:12Z
+
+
+ 2023-11-27T17:10:13Z
+
+
+ 2023-11-27T17:10:14Z
+
+
+ 2023-11-27T17:10:15Z
+
+
+ 2023-11-27T17:10:16Z
+
+
+ 2023-11-27T17:10:17Z
+
+
+ 2023-11-27T17:10:18Z
+
+
+ 2023-11-27T17:10:19Z
+
+
+ 2023-11-27T17:10:20Z
+
+
+ 2023-11-27T17:10:21Z
+
+
+ 2023-11-27T17:10:22Z
+
+
+ 2023-11-27T17:10:23Z
+
+
+ 2023-11-27T17:10:24Z
+
+
+ 2023-11-27T17:10:25Z
+
+
+ 2023-11-27T17:10:26Z
+
+
+ 2023-11-27T17:10:27Z
+
+
+ 2023-11-27T17:10:28Z
+
+
+ 2023-11-27T17:10:29Z
+
+
+ 2023-11-27T17:10:30Z
+
+
+ 2023-11-27T17:10:31Z
+
+
+ 2023-11-27T17:10:32Z
+
+
+ 2023-11-27T17:10:33Z
+
+
+ 2023-11-27T17:10:34Z
+
+
+ 2023-11-27T17:10:35Z
+
+
+ 2023-11-27T17:10:36Z
+
+
+ 2023-11-27T17:10:37Z
+
+
+ 2023-11-27T17:10:38Z
+
+
+ 2023-11-27T17:10:39Z
+
+
+ 2023-11-27T17:10:40Z
+
+
+ 2023-11-27T17:10:41Z
+
+
+ 2023-11-27T17:10:42Z
+
+
+ 2023-11-27T17:10:43Z
+
+
+ 2023-11-27T17:10:44Z
+
+
+ 2023-11-27T17:10:45Z
+
+
+ 2023-11-27T17:10:46Z
+
+
+ 2023-11-27T17:10:47Z
+
+
+ 2023-11-27T17:10:48Z
+
+
+ 2023-11-27T17:10:49Z
+
+
+ 2023-11-27T17:10:50Z
+
+
+ 2023-11-27T17:10:51Z
+
+
+ 2023-11-27T17:10:52Z
+
+
+ 2023-11-27T17:10:53Z
+
+
+ 2023-11-27T17:10:54Z
+
+
+ 2023-11-27T17:10:55Z
+
+
+ 2023-11-27T17:10:56Z
+
+
+ 2023-11-27T17:10:57Z
+
+
+ 2023-11-27T17:10:58Z
+
+
+ 2023-11-27T17:10:59Z
+
+
+ 2023-11-27T17:11:00Z
+
+
+ 2023-11-27T17:11:01Z
+
+
+ 2023-11-27T17:11:02Z
+
+
+ 2023-11-27T17:11:03Z
+
+
+ 2023-11-27T17:11:04Z
+
+
+ 2023-11-27T17:11:05Z
+
+
+ 2023-11-27T17:11:06Z
+
+
+ 2023-11-27T17:11:07Z
+
+
+ 2023-11-27T17:11:08Z
+
+
+ 2023-11-27T17:11:09Z
+
+
+ 2023-11-27T17:11:10Z
+
+
+ 2023-11-27T17:11:11Z
+
+
+ 2023-11-27T17:11:12Z
+
+
+ 2023-11-27T17:11:13Z
+
+
+ 2023-11-27T17:11:14Z
+
+
+ 2023-11-27T17:11:15Z
+
+
+ 2023-11-27T17:11:16Z
+
+
+ 2023-11-27T17:11:17Z
+
+
+ 2023-11-27T17:11:18Z
+
+
+ 2023-11-27T17:11:19Z
+
+
+ 2023-11-27T17:11:20Z
+
+
+ 2023-11-27T17:11:21Z
+
+
+ 2023-11-27T17:11:22Z
+
+
+ 2023-11-27T17:11:23Z
+
+
+ 2023-11-27T17:11:24Z
+
+
+ 2023-11-27T17:11:25Z
+
+
+ 2023-11-27T17:11:26Z
+
+
+ 2023-11-27T17:11:27Z
+
+
+ 2023-11-27T17:11:28Z
+
+
+ 2023-11-27T17:11:29Z
+
+
+ 2023-11-27T17:11:30Z
+
+
+ 2023-11-27T17:11:31Z
+
+
+ 2023-11-27T17:11:32Z
+
+
+ 2023-11-27T17:11:33Z
+
+
+ 2023-11-27T17:11:34Z
+
+
+ 2023-11-27T17:11:35Z
+
+
+ 2023-11-27T17:11:36Z
+
+
+ 2023-11-27T17:11:37Z
+
+
+ 2023-11-27T17:11:38Z
+
+
+ 2023-11-27T17:11:39Z
+
+
+ 2023-11-27T17:11:40Z
+
+
+ 2023-11-27T17:11:41Z
+
+
+ 2023-11-27T17:11:42Z
+
+
+ 2023-11-27T17:11:43Z
+
+
+ 2023-11-27T17:11:44Z
+
+
+ 2023-11-27T17:11:45Z
+
+
+ 2023-11-27T17:11:46Z
+
+
+ 2023-11-27T17:11:47Z
+
+
+ 2023-11-27T17:11:48Z
+
+
+ 2023-11-27T17:11:49Z
+
+
+ 2023-11-27T17:11:50Z
+
+
+ 2023-11-27T17:11:51Z
+
+
+ 2023-11-27T17:11:52Z
+
+
+ 2023-11-27T17:11:53Z
+
+
+ 2023-11-27T17:11:54Z
+
+
+ 2023-11-27T17:11:55Z
+
+
+ 2023-11-27T17:11:56Z
+
+
+ 2023-11-27T17:11:57Z
+
+
+ 2023-11-27T17:11:58Z
+
+
+ 2023-11-27T17:11:59Z
+
+
+ 2023-11-27T17:12:00Z
+
+
+ 2023-11-27T17:12:01Z
+
+
+ 2023-11-27T17:12:02Z
+
+
+ 2023-11-27T17:12:03Z
+
+
+ 2023-11-27T17:12:04Z
+
+
+ 2023-11-27T17:12:05Z
+
+
+ 2023-11-27T17:12:06Z
+
+
+ 2023-11-27T17:12:07Z
+
+
+ 2023-11-27T17:12:08Z
+
+
+ 2023-11-27T17:12:09Z
+
+
+ 2023-11-27T17:12:10Z
+
+
+ 2023-11-27T17:12:11Z
+
+
+ 2023-11-27T17:12:12Z
+
+
+ 2023-11-27T17:12:13Z
+
+
+ 2023-11-27T17:12:14Z
+
+
+ 2023-11-27T17:12:15Z
+
+
+ 2023-11-27T17:12:16Z
+
+
+ 2023-11-27T17:12:17Z
+
+
+ 2023-11-27T17:12:18Z
+
+
+ 2023-11-27T17:12:19Z
+
+
+ 2023-11-27T17:12:20Z
+
+
+ 2023-11-27T17:12:21Z
+
+
+ 2023-11-27T17:12:22Z
+
+
+ 2023-11-27T17:12:23Z
+
+
+ 2023-11-27T17:12:24Z
+
+
+ 2023-11-27T17:12:25Z
+
+
+ 2023-11-27T17:12:26Z
+
+
+ 2023-11-27T17:12:27Z
+
+
+ 2023-11-27T17:12:28Z
+
+
+ 2023-11-27T17:12:29Z
+
+
+ 2023-11-27T17:12:30Z
+
+
+ 2023-11-27T17:12:31Z
+
+
+ 2023-11-27T17:12:32Z
+
+
+ 2023-11-27T17:12:33Z
+
+
+ 2023-11-27T17:12:34Z
+
+
+ 2023-11-27T17:12:35Z
+
+
+ 2023-11-27T17:12:36Z
+
+
+ 2023-11-27T17:12:37Z
+
+
+ 2023-11-27T17:12:38Z
+
+
+ 2023-11-27T17:12:39Z
+
+
+ 2023-11-27T17:12:40Z
+
+
+ 2023-11-27T17:12:41Z
+
+
+ 2023-11-27T17:12:42Z
+
+
+ 2023-11-27T17:12:43Z
+
+
+ 2023-11-27T17:12:44Z
+
+
+ 2023-11-27T17:12:45Z
+
+
+ 2023-11-27T17:12:46Z
+
+
+ 2023-11-27T17:12:47Z
+
+
+ 2023-11-27T17:12:48Z
+
+
+ 2023-11-27T17:12:49Z
+
+
+ 2023-11-27T17:12:50Z
+
+
+ 2023-11-27T17:12:51Z
+
+
+ 2023-11-27T17:12:52Z
+
+
+ 2023-11-27T17:12:53Z
+
+
+ 2023-11-27T17:12:54Z
+
+
+ 2023-11-27T17:12:55Z
+
+
+ 2023-11-27T17:12:56Z
+
+
+ 2023-11-27T17:12:57Z
+
+
+ 2023-11-27T17:12:58Z
+
+
+ 2023-11-27T17:12:59Z
+
+
+ 2023-11-27T17:13:00Z
+
+
+ 2023-11-27T17:13:01Z
+
+
+ 2023-11-27T17:13:02Z
+
+
+ 2023-11-27T17:13:03Z
+
+
+ 2023-11-27T17:13:04Z
+
+
+ 2023-11-27T17:13:05Z
+
+
+ 2023-11-27T17:13:06Z
+
+
+ 2023-11-27T17:13:07Z
+
+
+ 2023-11-27T17:13:08Z
+
+
+ 2023-11-27T17:13:09Z
+
+
+ 2023-11-27T17:13:10Z
+
+
+ 2023-11-27T17:13:11Z
+
+
+ 2023-11-27T17:13:12Z
+
+
+ 2023-11-27T17:13:13Z
+
+
+ 2023-11-27T17:13:14Z
+
+
+ 2023-11-27T17:13:15Z
+
+
+ 2023-11-27T17:13:16Z
+
+
+ 2023-11-27T17:13:17Z
+
+
+ 2023-11-27T17:13:18Z
+
+
+ 2023-11-27T17:13:19Z
+
+
+ 2023-11-27T17:13:20Z
+
+
+ 2023-11-27T17:13:21Z
+
+
+ 2023-11-27T17:13:22Z
+
+
+ 2023-11-27T17:13:23Z
+
+
+ 2023-11-27T17:13:24Z
+
+
+ 2023-11-27T17:13:25Z
+
+
+ 2023-11-27T17:13:26Z
+
+
+ 2023-11-27T17:13:27Z
+
+
+ 2023-11-27T17:13:28Z
+
+
+ 2023-11-27T17:13:29Z
+
+
+ 2023-11-27T17:13:30Z
+
+
+ 2023-11-27T17:13:31Z
+
+
+ 2023-11-27T17:13:32Z
+
+
+ 2023-11-27T17:13:33Z
+
+
+ 2023-11-27T17:13:34Z
+
+
+ 2023-11-27T17:13:35Z
+
+
+ 2023-11-27T17:13:36Z
+
+
+ 2023-11-27T17:13:37Z
+
+
+ 2023-11-27T17:13:38Z
+
+
+ 2023-11-27T17:13:39Z
+
+
+ 2023-11-27T17:13:40Z
+
+
+ 2023-11-27T17:13:41Z
+
+
+ 2023-11-27T17:13:42Z
+
+
+ 2023-11-27T17:13:43Z
+
+
+ 2023-11-27T17:13:44Z
+
+
+ 2023-11-27T17:13:45Z
+
+
+ 2023-11-27T17:13:46Z
+
+
+ 2023-11-27T17:13:47Z
+
+
+ 2023-11-27T17:13:48Z
+
+
+ 2023-11-27T17:13:49Z
+
+
+ 2023-11-27T17:13:50Z
+
+
+ 2023-11-27T17:13:51Z
+
+
+ 2023-11-27T17:13:52Z
+
+
+ 2023-11-27T17:13:53Z
+
+
+ 2023-11-27T17:13:54Z
+
+
+ 2023-11-27T17:13:55Z
+
+
+ 2023-11-27T17:13:56Z
+
+
+ 2023-11-27T17:13:57Z
+
+
+ 2023-11-27T17:13:58Z
+
+
+ 2023-11-27T17:13:59Z
+
+
+ 2023-11-27T17:14:00Z
+
+
+ 2023-11-27T17:14:01Z
+
+
+ 2023-11-27T17:14:02Z
+
+
+ 2023-11-27T17:14:03Z
+
+
+ 2023-11-27T17:14:04Z
+
+
+ 2023-11-27T17:14:05Z
+
+
+ 2023-11-27T17:14:06Z
+
+
+ 2023-11-27T17:14:07Z
+
+
+ 2023-11-27T17:14:08Z
+
+
+ 2023-11-27T17:14:09Z
+
+
+ 2023-11-27T17:14:10Z
+
+
+ 2023-11-27T17:14:11Z
+
+
+ 2023-11-27T17:14:12Z
+
+
+ 2023-11-27T17:14:13Z
+
+
+ 2023-11-27T17:14:14Z
+
+
+ 2023-11-27T17:14:15Z
+
+
+ 2023-11-27T17:14:16Z
+
+
+ 2023-11-27T17:14:17Z
+
+
+ 2023-11-27T17:14:18Z
+
+
+ 2023-11-27T17:14:19Z
+
+
+ 2023-11-27T17:14:20Z
+
+
+ 2023-11-27T17:14:21Z
+
+
+ 2023-11-27T17:14:22Z
+
+
+ 2023-11-27T17:14:23Z
+
+
+ 2023-11-27T17:14:24Z
+
+
+ 2023-11-27T17:14:25Z
+
+
+ 2023-11-27T17:14:26Z
+
+
+ 2023-11-27T17:14:27Z
+
+
+ 2023-11-27T17:14:28Z
+
+
+ 2023-11-27T17:14:29Z
+
+
+ 2023-11-27T17:14:30Z
+
+
+ 2023-11-27T17:14:31Z
+
+
+ 2023-11-27T17:14:32Z
+
+
+ 2023-11-27T17:14:33Z
+
+
+ 2023-11-27T17:14:34Z
+
+
+ 2023-11-27T17:14:35Z
+
+
+ 2023-11-27T17:14:36Z
+
+
+
+
+ 20231127081350.gpx
+ Daily routes
+ /user/AmOosm/traces/11276805
+
+
+ 2023-11-27T07:13:53Z
+
+
+ 2023-11-27T07:13:54Z
+
+
+ 2023-11-27T07:13:55Z
+
+
+ 2023-11-27T07:13:56Z
+
+
+ 2023-11-27T07:13:57Z
+
+
+ 2023-11-27T07:13:58Z
+
+
+ 2023-11-27T07:13:59Z
+
+
+ 2023-11-27T07:14:00Z
+
+
+ 2023-11-27T07:14:01Z
+
+
+ 2023-11-27T07:14:02Z
+
+
+ 2023-11-27T07:14:03Z
+
+
+ 2023-11-27T07:14:04Z
+
+
+ 2023-11-27T07:14:05Z
+
+
+ 2023-11-27T07:14:06Z
+
+
+ 2023-11-27T07:14:07Z
+
+
+ 2023-11-27T07:14:08Z
+
+
+ 2023-11-27T07:14:09Z
+
+
+ 2023-11-27T07:14:10Z
+
+
+ 2023-11-27T07:14:11Z
+
+
+ 2023-11-27T07:14:12Z
+
+
+ 2023-11-27T07:14:13Z
+
+
+ 2023-11-27T07:14:14Z
+
+
+ 2023-11-27T07:14:15Z
+
+
+ 2023-11-27T07:14:16Z
+
+
+ 2023-11-27T07:14:17Z
+
+
+ 2023-11-27T07:14:18Z
+
+
+ 2023-11-27T07:14:19Z
+
+
+ 2023-11-27T07:14:20Z
+
+
+ 2023-11-27T07:14:21Z
+
+
+ 2023-11-27T07:14:22Z
+
+
+ 2023-11-27T07:14:23Z
+
+
+ 2023-11-27T07:14:24Z
+
+
+ 2023-11-27T07:14:25Z
+
+
+ 2023-11-27T07:14:26Z
+
+
+ 2023-11-27T07:14:27Z
+
+
+ 2023-11-27T07:14:28Z
+
+
+ 2023-11-27T07:14:29Z
+
+
+ 2023-11-27T07:14:30Z
+
+
+ 2023-11-27T07:14:31Z
+
+
+ 2023-11-27T07:14:32Z
+
+
+ 2023-11-27T07:14:33Z
+
+
+ 2023-11-27T07:14:34Z
+
+
+ 2023-11-27T07:14:35Z
+
+
+ 2023-11-27T07:14:36Z
+
+
+ 2023-11-27T07:14:37Z
+
+
+ 2023-11-27T07:14:38Z
+
+
+ 2023-11-27T07:14:39Z
+
+
+ 2023-11-27T07:14:40Z
+
+
+ 2023-11-27T07:14:41Z
+
+
+ 2023-11-27T07:14:42Z
+
+
+ 2023-11-27T07:14:43Z
+
+
+ 2023-11-27T07:14:44Z
+
+
+ 2023-11-27T07:14:45Z
+
+
+ 2023-11-27T07:14:46Z
+
+
+ 2023-11-27T07:14:47Z
+
+
+ 2023-11-27T07:14:48Z
+
+
+ 2023-11-27T07:14:49Z
+
+
+ 2023-11-27T07:14:50Z
+
+
+ 2023-11-27T07:14:51Z
+
+
+ 2023-11-27T07:14:52Z
+
+
+ 2023-11-27T07:14:53Z
+
+
+ 2023-11-27T07:14:54Z
+
+
+ 2023-11-27T07:14:55Z
+
+
+ 2023-11-27T07:14:56Z
+
+
+ 2023-11-27T07:14:57Z
+
+
+ 2023-11-27T07:14:58Z
+
+
+ 2023-11-27T07:14:59Z
+
+
+ 2023-11-27T07:15:00Z
+
+
+ 2023-11-27T07:15:01Z
+
+
+ 2023-11-27T07:15:02Z
+
+
+ 2023-11-27T07:15:03Z
+
+
+ 2023-11-27T07:15:04Z
+
+
+ 2023-11-27T07:15:05Z
+
+
+ 2023-11-27T07:15:06Z
+
+
+ 2023-11-27T07:15:07Z
+
+
+ 2023-11-27T07:15:08Z
+
+
+ 2023-11-27T07:15:09Z
+
+
+ 2023-11-27T07:15:10Z
+
+
+ 2023-11-27T07:15:11Z
+
+
+ 2023-11-27T07:15:12Z
+
+
+ 2023-11-27T07:15:13Z
+
+
+ 2023-11-27T07:15:14Z
+
+
+ 2023-11-27T07:15:15Z
+
+
+ 2023-11-27T07:15:16Z
+
+
+ 2023-11-27T07:15:17Z
+
+
+ 2023-11-27T07:15:18Z
+
+
+ 2023-11-27T07:15:19Z
+
+
+ 2023-11-27T07:15:20Z
+
+
+ 2023-11-27T07:15:21Z
+
+
+ 2023-11-27T07:15:22Z
+
+
+ 2023-11-27T07:15:23Z
+
+
+ 2023-11-27T07:15:24Z
+
+
+ 2023-11-27T07:15:25Z
+
+
+ 2023-11-27T07:15:26Z
+
+
+ 2023-11-27T07:15:27Z
+
+
+ 2023-11-27T07:15:28Z
+
+
+ 2023-11-27T07:15:29Z
+
+
+ 2023-11-27T07:15:30Z
+
+
+ 2023-11-27T07:15:31Z
+
+
+ 2023-11-27T07:15:32Z
+
+
+ 2023-11-27T07:15:33Z
+
+
+ 2023-11-27T07:15:34Z
+
+
+ 2023-11-27T07:15:35Z
+
+
+ 2023-11-27T07:15:36Z
+
+
+ 2023-11-27T07:15:37Z
+
+
+ 2023-11-27T07:15:38Z
+
+
+ 2023-11-27T07:15:39Z
+
+
+ 2023-11-27T07:15:40Z
+
+
+ 2023-11-27T07:15:41Z
+
+
+ 2023-11-27T07:15:42Z
+
+
+ 2023-11-27T07:15:43Z
+
+
+ 2023-11-27T07:15:44Z
+
+
+ 2023-11-27T07:15:45Z
+
+
+ 2023-11-27T07:15:46Z
+
+
+ 2023-11-27T07:15:47Z
+
+
+ 2023-11-27T07:15:48Z
+
+
+ 2023-11-27T07:15:49Z
+
+
+ 2023-11-27T07:15:50Z
+
+
+ 2023-11-27T07:15:51Z
+
+
+ 2023-11-27T07:15:52Z
+
+
+ 2023-11-27T07:15:53Z
+
+
+ 2023-11-27T07:15:54Z
+
+
+ 2023-11-27T07:15:55Z
+
+
+ 2023-11-27T07:15:56Z
+
+
+ 2023-11-27T07:15:57Z
+
+
+ 2023-11-27T07:15:58Z
+
+
+ 2023-11-27T07:15:59Z
+
+
+ 2023-11-27T07:16:00Z
+
+
+ 2023-11-27T07:16:01Z
+
+
+ 2023-11-27T07:16:02Z
+
+
+ 2023-11-27T07:16:03Z
+
+
+ 2023-11-27T07:16:04Z
+
+
+ 2023-11-27T07:16:05Z
+
+
+ 2023-11-27T07:16:06Z
+
+
+ 2023-11-27T07:16:07Z
+
+
+ 2023-11-27T07:16:08Z
+
+
+ 2023-11-27T07:16:09Z
+
+
+ 2023-11-27T07:16:10Z
+
+
+ 2023-11-27T07:16:11Z
+
+
+ 2023-11-27T07:16:12Z
+
+
+ 2023-11-27T07:16:13Z
+
+
+ 2023-11-27T07:16:14Z
+
+
+ 2023-11-27T07:16:15Z
+
+
+ 2023-11-27T07:16:16Z
+
+
+ 2023-11-27T07:16:17Z
+
+
+ 2023-11-27T07:16:18Z
+
+
+ 2023-11-27T07:16:19Z
+
+
+ 2023-11-27T07:16:20Z
+
+
+ 2023-11-27T07:16:21Z
+
+
+ 2023-11-27T07:16:22Z
+
+
+ 2023-11-27T07:16:23Z
+
+
+ 2023-11-27T07:16:24Z
+
+
+ 2023-11-27T07:16:25Z
+
+
+ 2023-11-27T07:16:26Z
+
+
+ 2023-11-27T07:16:27Z
+
+
+ 2023-11-27T07:16:28Z
+
+
+ 2023-11-27T07:16:29Z
+
+
+ 2023-11-27T07:16:30Z
+
+
+ 2023-11-27T07:16:31Z
+
+
+ 2023-11-27T07:16:32Z
+
+
+ 2023-11-27T07:16:33Z
+
+
+ 2023-11-27T07:16:34Z
+
+
+ 2023-11-27T07:16:35Z
+
+
+ 2023-11-27T07:16:36Z
+
+
+ 2023-11-27T07:16:37Z
+
+
+ 2023-11-27T07:16:38Z
+
+
+ 2023-11-27T07:16:39Z
+
+
+ 2023-11-27T07:16:40Z
+
+
+ 2023-11-27T07:16:41Z
+
+
+ 2023-11-27T07:16:42Z
+
+
+ 2023-11-27T07:16:43Z
+
+
+ 2023-11-27T07:16:44Z
+
+
+ 2023-11-27T07:16:45Z
+
+
+ 2023-11-27T07:16:46Z
+
+
+ 2023-11-27T07:16:47Z
+
+
+ 2023-11-27T07:16:48Z
+
+
+ 2023-11-27T07:16:49Z
+
+
+ 2023-11-27T07:16:50Z
+
+
+ 2023-11-27T07:16:51Z
+
+
+ 2023-11-27T07:16:52Z
+
+
+ 2023-11-27T07:16:53Z
+
+
+ 2023-11-27T07:16:54Z
+
+
+ 2023-11-27T07:16:55Z
+
+
+ 2023-11-27T07:16:56Z
+
+
+ 2023-11-27T07:16:57Z
+
+
+ 2023-11-27T07:16:58Z
+
+
+ 2023-11-27T07:16:59Z
+
+
+ 2023-11-27T07:17:00Z
+
+
+ 2023-11-27T07:17:01Z
+
+
+ 2023-11-27T07:17:02Z
+
+
+ 2023-11-27T07:17:03Z
+
+
+ 2023-11-27T07:17:04Z
+
+
+ 2023-11-27T07:17:05Z
+
+
+ 2023-11-27T07:17:06Z
+
+
+ 2023-11-27T07:17:07Z
+
+
+ 2023-11-27T07:17:08Z
+
+
+ 2023-11-27T07:17:09Z
+
+
+ 2023-11-27T07:17:10Z
+
+
+ 2023-11-27T07:17:11Z
+
+
+ 2023-11-27T07:17:12Z
+
+
+ 2023-11-27T07:17:13Z
+
+
+ 2023-11-27T07:17:14Z
+
+
+ 2023-11-27T07:17:15Z
+
+
+ 2023-11-27T07:17:16Z
+
+
+ 2023-11-27T07:17:17Z
+
+
+ 2023-11-27T07:17:18Z
+
+
+ 2023-11-27T07:17:19Z
+
+
+ 2023-11-27T07:17:20Z
+
+
+
+
+ 20231126103258.gpx
+ Daily routes
+ /user/AmOosm/traces/11276793
+
+
+ 2023-11-26T10:07:32Z
+
+
+ 2023-11-26T10:07:33Z
+
+
+ 2023-11-26T10:07:34Z
+
+
+ 2023-11-26T10:07:35Z
+
+
+ 2023-11-26T10:07:36Z
+
+
+ 2023-11-26T10:07:37Z
+
+
+ 2023-11-26T10:07:38Z
+
+
+ 2023-11-26T10:07:39Z
+
+
+ 2023-11-26T10:07:40Z
+
+
+ 2023-11-26T10:07:41Z
+
+
+ 2023-11-26T10:07:42Z
+
+
+ 2023-11-26T10:07:43Z
+
+
+ 2023-11-26T10:07:44Z
+
+
+ 2023-11-26T10:07:45Z
+
+
+ 2023-11-26T10:07:46Z
+
+
+ 2023-11-26T10:07:47Z
+
+
+ 2023-11-26T10:07:48Z
+
+
+ 2023-11-26T10:07:49Z
+
+
+ 2023-11-26T10:07:50Z
+
+
+ 2023-11-26T10:07:51Z
+
+
+ 2023-11-26T10:07:52Z
+
+
+ 2023-11-26T10:07:53Z
+
+
+ 2023-11-26T10:07:54Z
+
+
+ 2023-11-26T10:07:55Z
+
+
+ 2023-11-26T10:07:56Z
+
+
+ 2023-11-26T10:07:57Z
+
+
+ 2023-11-26T10:07:58Z
+
+
+ 2023-11-26T10:07:59Z
+
+
+ 2023-11-26T10:08:00Z
+
+
+ 2023-11-26T10:08:01Z
+
+
+ 2023-11-26T10:08:02Z
+
+
+ 2023-11-26T10:08:03Z
+
+
+ 2023-11-26T10:08:04Z
+
+
+ 2023-11-26T10:08:05Z
+
+
+ 2023-11-26T10:08:06Z
+
+
+ 2023-11-26T10:08:07Z
+
+
+ 2023-11-26T10:08:08Z
+
+
+ 2023-11-26T10:08:09Z
+
+
+ 2023-11-26T10:08:10Z
+
+
+ 2023-11-26T10:08:11Z
+
+
+ 2023-11-26T10:08:12Z
+
+
+ 2023-11-26T10:08:13Z
+
+
+ 2023-11-26T10:08:14Z
+
+
+ 2023-11-26T10:08:15Z
+
+
+ 2023-11-26T10:08:16Z
+
+
+ 2023-11-26T10:08:17Z
+
+
+ 2023-11-26T10:08:18Z
+
+
+ 2023-11-26T10:08:19Z
+
+
+ 2023-11-26T10:08:20Z
+
+
+ 2023-11-26T10:08:21Z
+
+
+ 2023-11-26T10:08:22Z
+
+
+ 2023-11-26T10:08:23Z
+
+
+ 2023-11-26T10:08:24Z
+
+
+ 2023-11-26T10:08:25Z
+
+
+ 2023-11-26T10:08:26Z
+
+
+ 2023-11-26T10:08:27Z
+
+
+ 2023-11-26T10:08:28Z
+
+
+ 2023-11-26T10:08:29Z
+
+
+ 2023-11-26T10:08:30Z
+
+
+ 2023-11-26T10:08:31Z
+
+
+ 2023-11-26T10:08:32Z
+
+
+ 2023-11-26T10:08:33Z
+
+
+ 2023-11-26T10:08:34Z
+
+
+ 2023-11-26T10:08:35Z
+
+
+ 2023-11-26T10:08:36Z
+
+
+ 2023-11-26T10:08:37Z
+
+
+ 2023-11-26T10:08:38Z
+
+
+ 2023-11-26T10:08:39Z
+
+
+ 2023-11-26T10:08:40Z
+
+
+ 2023-11-26T10:08:41Z
+
+
+ 2023-11-26T10:08:42Z
+
+
+ 2023-11-26T10:08:43Z
+
+
+ 2023-11-26T10:08:44Z
+
+
+ 2023-11-26T10:08:45Z
+
+
+ 2023-11-26T10:08:46Z
+
+
+ 2023-11-26T10:08:47Z
+
+
+ 2023-11-26T10:08:48Z
+
+
+ 2023-11-26T10:08:49Z
+
+
+ 2023-11-26T10:08:50Z
+
+
+ 2023-11-26T10:08:51Z
+
+
+ 2023-11-26T10:08:52Z
+
+
+ 2023-11-26T10:08:53Z
+
+
+ 2023-11-26T10:08:54Z
+
+
+ 2023-11-26T10:08:55Z
+
+
+ 2023-11-26T10:08:56Z
+
+
+ 2023-11-26T10:08:57Z
+
+
+ 2023-11-26T10:08:59Z
+
+
+ 2023-11-26T10:09:00Z
+
+
+ 2023-11-26T10:09:02Z
+
+
+ 2023-11-26T10:09:03Z
+
+
+ 2023-11-26T10:09:04Z
+
+
+ 2023-11-26T10:09:05Z
+
+
+ 2023-11-26T10:09:06Z
+
+
+ 2023-11-26T10:09:07Z
+
+
+ 2023-11-26T10:09:08Z
+
+
+ 2023-11-26T10:09:09Z
+
+
+ 2023-11-26T10:09:10Z
+
+
+ 2023-11-26T10:09:11Z
+
+
+ 2023-11-26T10:09:12Z
+
+
+ 2023-11-26T10:09:13Z
+
+
+ 2023-11-26T10:09:14Z
+
+
+ 2023-11-26T10:09:15Z
+
+
+ 2023-11-26T10:09:16Z
+
+
+ 2023-11-26T10:09:17Z
+
+
+ 2023-11-26T10:09:18Z
+
+
+ 2023-11-26T10:09:19Z
+
+
+ 2023-11-26T10:09:20Z
+
+
+ 2023-11-26T10:09:21Z
+
+
+ 2023-11-26T10:09:22Z
+
+
+ 2023-11-26T10:09:23Z
+
+
+ 2023-11-26T10:09:24Z
+
+
+ 2023-11-26T10:09:25Z
+
+
+ 2023-11-26T10:09:26Z
+
+
+ 2023-11-26T10:09:27Z
+
+
+ 2023-11-26T10:09:28Z
+
+
+ 2023-11-26T10:09:29Z
+
+
+ 2023-11-26T10:09:30Z
+
+
+ 2023-11-26T10:09:31Z
+
+
+ 2023-11-26T10:09:32Z
+
+
+ 2023-11-26T10:09:33Z
+
+
+ 2023-11-26T10:09:34Z
+
+
+ 2023-11-26T10:09:35Z
+
+
+ 2023-11-26T10:09:36Z
+
+
+ 2023-11-26T10:09:37Z
+
+
+ 2023-11-26T10:09:38Z
+
+
+ 2023-11-26T10:09:39Z
+
+
+ 2023-11-26T10:09:40Z
+
+
+ 2023-11-26T10:09:41Z
+
+
+ 2023-11-26T10:09:42Z
+
+
+ 2023-11-26T10:09:43Z
+
+
+ 2023-11-26T10:09:44Z
+
+
+ 2023-11-26T10:09:45Z
+
+
+ 2023-11-26T10:09:46Z
+
+
+ 2023-11-26T10:09:47Z
+
+
+ 2023-11-26T10:09:48Z
+
+
+ 2023-11-26T10:09:49Z
+
+
+ 2023-11-26T10:09:50Z
+
+
+ 2023-11-26T10:09:51Z
+
+
+ 2023-11-26T10:09:52Z
+
+
+ 2023-11-26T10:09:53Z
+
+
+ 2023-11-26T10:09:54Z
+
+
+ 2023-11-26T10:09:55Z
+
+
+ 2023-11-26T10:09:56Z
+
+
+ 2023-11-26T10:09:57Z
+
+
+ 2023-11-26T10:09:58Z
+
+
+ 2023-11-26T10:09:59Z
+
+
+ 2023-11-26T10:10:00Z
+
+
+ 2023-11-26T10:10:01Z
+
+
+ 2023-11-26T10:10:02Z
+
+
+ 2023-11-26T10:10:03Z
+
+
+ 2023-11-26T10:10:04Z
+
+
+ 2023-11-26T10:10:05Z
+
+
+ 2023-11-26T10:10:06Z
+
+
+ 2023-11-26T10:10:07Z
+
+
+ 2023-11-26T10:10:08Z
+
+
+ 2023-11-26T10:10:09Z
+
+
+ 2023-11-26T10:10:10Z
+
+
+ 2023-11-26T10:10:11Z
+
+
+ 2023-11-26T10:10:12Z
+
+
+ 2023-11-26T10:10:13Z
+
+
+ 2023-11-26T10:10:14Z
+
+
+ 2023-11-26T10:10:15Z
+
+
+ 2023-11-26T10:10:16Z
+
+
+ 2023-11-26T10:10:17Z
+
+
+ 2023-11-26T10:10:18Z
+
+
+ 2023-11-26T10:10:19Z
+
+
+ 2023-11-26T10:10:20Z
+
+
+ 2023-11-26T10:10:21Z
+
+
+ 2023-11-26T10:10:22Z
+
+
+ 2023-11-26T10:10:23Z
+
+
+ 2023-11-26T10:10:24Z
+
+
+ 2023-11-26T10:10:25Z
+
+
+ 2023-11-26T10:10:26Z
+
+
+ 2023-11-26T10:10:27Z
+
+
+ 2023-11-26T10:10:28Z
+
+
+ 2023-11-26T10:10:29Z
+
+
+ 2023-11-26T10:10:30Z
+
+
+ 2023-11-26T10:10:31Z
+
+
+ 2023-11-26T10:10:32Z
+
+
+ 2023-11-26T10:10:33Z
+
+
+ 2023-11-26T10:10:34Z
+
+
+ 2023-11-26T10:10:35Z
+
+
+ 2023-11-26T10:10:36Z
+
+
+ 2023-11-26T10:10:37Z
+
+
+ 2023-11-26T10:10:38Z
+
+
+ 2023-11-26T10:10:39Z
+
+
+ 2023-11-26T10:10:40Z
+
+
+ 2023-11-26T10:10:41Z
+
+
+ 2023-11-26T10:10:42Z
+
+
+ 2023-11-26T10:10:43Z
+
+
+ 2023-11-26T10:10:44Z
+
+
+ 2023-11-26T10:10:45Z
+
+
+ 2023-11-26T10:10:46Z
+
+
+ 2023-11-26T10:10:47Z
+
+
+ 2023-11-26T10:10:48Z
+
+
+ 2023-11-26T10:10:49Z
+
+
+ 2023-11-26T10:10:50Z
+
+
+ 2023-11-26T10:10:51Z
+
+
+ 2023-11-26T10:10:52Z
+
+
+ 2023-11-26T10:10:53Z
+
+
+ 2023-11-26T10:10:54Z
+
+
+ 2023-11-26T10:10:55Z
+
+
+ 2023-11-26T10:10:56Z
+
+
+ 2023-11-26T10:10:57Z
+
+
+ 2023-11-26T10:10:58Z
+
+
+ 2023-11-26T10:10:59Z
+
+
+ 2023-11-26T10:11:00Z
+
+
+ 2023-11-26T10:11:01Z
+
+
+ 2023-11-26T10:11:02Z
+
+
+ 2023-11-26T10:11:03Z
+
+
+ 2023-11-26T10:11:04Z
+
+
+ 2023-11-26T10:11:05Z
+
+
+ 2023-11-26T10:11:06Z
+
+
+ 2023-11-26T10:11:07Z
+
+
+ 2023-11-26T10:11:08Z
+
+
+ 2023-11-26T10:11:09Z
+
+
+ 2023-11-26T10:11:10Z
+
+
+ 2023-11-26T10:11:11Z
+
+
+ 2023-11-26T10:11:12Z
+
+
+ 2023-11-26T10:11:13Z
+
+
+ 2023-11-26T10:11:14Z
+
+
+ 2023-11-26T10:11:15Z
+
+
+ 2023-11-26T10:11:16Z
+
+
+ 2023-11-26T10:11:17Z
+
+
+ 2023-11-26T10:11:18Z
+
+
+ 2023-11-26T10:11:19Z
+
+
+ 2023-11-26T10:11:20Z
+
+
+ 2023-11-26T10:11:21Z
+
+
+ 2023-11-26T10:11:22Z
+
+
+ 2023-11-26T10:11:23Z
+
+
+ 2023-11-26T10:11:24Z
+
+
+ 2023-11-26T10:11:25Z
+
+
+ 2023-11-26T10:11:26Z
+
+
+ 2023-11-26T10:11:27Z
+
+
+ 2023-11-26T10:11:28Z
+
+
+ 2023-11-26T10:11:29Z
+
+
+ 2023-11-26T10:11:30Z
+
+
+ 2023-11-26T10:11:31Z
+
+
+ 2023-11-26T10:11:32Z
+
+
+ 2023-11-26T10:11:33Z
+
+
+ 2023-11-26T10:11:34Z
+
+
+ 2023-11-26T10:11:35Z
+
+
+ 2023-11-26T10:11:36Z
+
+
+ 2023-11-26T10:11:37Z
+
+
+ 2023-11-26T10:11:38Z
+
+
+ 2023-11-26T10:11:39Z
+
+
+ 2023-11-26T10:11:40Z
+
+
+ 2023-11-26T10:11:41Z
+
+
+ 2023-11-26T10:11:42Z
+
+
+ 2023-11-26T10:11:43Z
+
+
+ 2023-11-26T10:11:44Z
+
+
+ 2023-11-26T10:11:45Z
+
+
+ 2023-11-26T10:11:46Z
+
+
+ 2023-11-26T10:11:47Z
+
+
+ 2023-11-26T10:11:48Z
+
+
+ 2023-11-26T10:11:49Z
+
+
+ 2023-11-26T10:11:50Z
+
+
+ 2023-11-26T10:11:51Z
+
+
+ 2023-11-26T10:11:52Z
+
+
+ 2023-11-26T10:11:53Z
+
+
+ 2023-11-26T10:11:54Z
+
+
+ 2023-11-26T10:11:55Z
+
+
+ 2023-11-26T10:11:56Z
+
+
+ 2023-11-26T10:11:57Z
+
+
+ 2023-11-26T10:11:58Z
+
+
+ 2023-11-26T10:11:59Z
+
+
+ 2023-11-26T10:12:00Z
+
+
+ 2023-11-26T10:12:01Z
+
+
+ 2023-11-26T10:12:02Z
+
+
+ 2023-11-26T10:12:03Z
+
+
+ 2023-11-26T10:12:04Z
+
+
+ 2023-11-26T10:12:05Z
+
+
+ 2023-11-26T10:12:06Z
+
+
+ 2023-11-26T10:12:07Z
+
+
+ 2023-11-26T10:12:08Z
+
+
+ 2023-11-26T10:12:09Z
+
+
+ 2023-11-26T10:12:10Z
+
+
+ 2023-11-26T10:12:11Z
+
+
+ 2023-11-26T10:12:12Z
+
+
+ 2023-11-26T10:12:13Z
+
+
+ 2023-11-26T10:12:14Z
+
+
+ 2023-11-26T10:12:15Z
+
+
+ 2023-11-26T10:12:16Z
+
+
+ 2023-11-26T10:12:17Z
+
+
+ 2023-11-26T10:12:18Z
+
+
+ 2023-11-26T10:12:19Z
+
+
+ 2023-11-26T10:12:20Z
+
+
+ 2023-11-26T10:12:21Z
+
+
+ 2023-11-26T10:12:22Z
+
+
+ 2023-11-26T10:12:23Z
+
+
+ 2023-11-26T10:12:24Z
+
+
+ 2023-11-26T10:12:25Z
+
+
+ 2023-11-26T10:12:26Z
+
+
+ 2023-11-26T10:12:27Z
+
+
+ 2023-11-26T10:12:28Z
+
+
+ 2023-11-26T10:12:29Z
+
+
+ 2023-11-26T10:12:30Z
+
+
+ 2023-11-26T10:12:31Z
+
+
+ 2023-11-26T10:12:32Z
+
+
+ 2023-11-26T10:12:33Z
+
+
+ 2023-11-26T10:12:34Z
+
+
+ 2023-11-26T10:12:35Z
+
+
+ 2023-11-26T10:12:36Z
+
+
+ 2023-11-26T10:12:37Z
+
+
+ 2023-11-26T10:12:38Z
+
+
+ 2023-11-26T10:12:39Z
+
+
+ 2023-11-26T10:12:40Z
+
+
+ 2023-11-26T10:12:41Z
+
+
+
+
+ Nocny_spacer_przez_Wisle_i_Park.gpx
+ Nocny spacer przez Wisle i Park
+ /user/GanderPL/traces/11217138
+
+
+ 2023-12-06T22:08:01Z
+
+
+ 2023-12-06T22:08:05Z
+
+
+ 2023-12-06T22:08:09Z
+
+
+ 2023-12-06T22:08:12Z
+
+
+ 2023-12-06T22:08:16Z
+
+
+ 2023-12-06T22:08:19Z
+
+
+ 2023-12-06T22:08:22Z
+
+
+ 2023-12-06T22:08:25Z
+
+
+ 2023-12-06T22:08:29Z
+
+
+ 2023-12-06T22:08:32Z
+
+
+ 2023-12-06T22:08:36Z
+
+
+ 2023-12-06T22:08:39Z
+
+
+ 2023-12-06T22:08:43Z
+
+
+ 2023-12-06T22:08:46Z
+
+
+ 2023-12-06T22:08:50Z
+
+
+ 2023-12-06T22:08:54Z
+
+
+ 2023-12-06T22:08:57Z
+
+
+ 2023-12-06T22:09:01Z
+
+
+ 2023-12-06T22:09:05Z
+
+
+ 2023-12-06T22:09:09Z
+
+
+ 2023-12-06T22:09:12Z
+
+
+ 2023-12-06T22:09:16Z
+
+
+ 2023-12-06T22:09:19Z
+
+
+ 2023-12-06T22:09:23Z
+
+
+ 2023-12-06T22:09:26Z
+
+
+ 2023-12-06T22:09:30Z
+
+
+ 2023-12-06T22:09:33Z
+
+
+ 2023-12-06T22:09:37Z
+
+
+ 2023-12-06T22:09:41Z
+
+
+ 2023-12-06T22:09:44Z
+
+
+ 2023-12-06T22:09:47Z
+
+
+ 2023-12-06T22:09:51Z
+
+
+ 2023-12-06T22:09:55Z
+
+
+ 2023-12-06T22:09:58Z
+
+
+ 2023-12-06T22:10:02Z
+
+
+ 2023-12-06T22:10:06Z
+
+
+ 2023-12-06T22:10:10Z
+
+
+ 2023-12-06T22:10:13Z
+
+
+ 2023-12-06T22:10:16Z
+
+
+ 2023-12-06T22:10:21Z
+
+
+ 2023-12-06T22:10:24Z
+
+
+ 2023-12-06T22:10:27Z
+
+
+ 2023-12-06T22:10:30Z
+
+
+ 2023-12-06T22:10:33Z
+
+
+ 2023-12-06T22:10:38Z
+
+
+ 2023-12-06T22:10:41Z
+
+
+ 2023-12-06T22:10:44Z
+
+
+ 2023-12-06T22:10:48Z
+
+
+ 2023-12-06T22:10:51Z
+
+
+ 2023-12-06T22:10:55Z
+
+
+ 2023-12-06T22:10:59Z
+
+
+ 2023-12-06T22:11:02Z
+
+
+ 2023-12-06T22:11:06Z
+
+
+ 2023-12-06T22:11:10Z
+
+
+ 2023-12-06T22:11:13Z
+
+
+ 2023-12-06T22:11:16Z
+
+
+ 2023-12-06T22:11:21Z
+
+
+ 2023-12-06T22:11:24Z
+
+
+ 2023-12-06T22:11:28Z
+
+
+ 2023-12-06T22:11:32Z
+
+
+ 2023-12-06T22:11:37Z
+
+
+ 2023-12-06T22:11:40Z
+
+
+ 2023-12-06T22:11:44Z
+
+
+ 2023-12-06T22:11:47Z
+
+
+ 2023-12-06T22:11:51Z
+
+
+ 2023-12-06T22:11:54Z
+
+
+ 2023-12-06T22:11:57Z
+
+
+ 2023-12-06T22:12:01Z
+
+
+ 2023-12-06T22:12:05Z
+
+
+ 2023-12-06T22:12:08Z
+
+
+ 2023-12-06T22:12:12Z
+
+
+ 2023-12-06T22:12:15Z
+
+
+ 2023-12-06T22:12:18Z
+
+
+ 2023-12-06T22:12:22Z
+
+
+ 2023-12-06T22:12:25Z
+
+
+ 2023-12-06T22:12:29Z
+
+
+ 2023-12-06T22:12:32Z
+
+
+ 2023-12-06T22:12:36Z
+
+
+ 2023-12-06T22:12:40Z
+
+
+ 2023-12-06T22:12:43Z
+
+
+ 2023-12-06T22:12:46Z
+
+
+ 2023-12-06T22:12:50Z
+
+
+ 2023-12-06T22:12:53Z
+
+
+ 2023-12-06T22:12:56Z
+
+
+ 2023-12-06T22:12:59Z
+
+
+ 2023-12-06T22:13:03Z
+
+
+ 2023-12-06T22:13:07Z
+
+
+ 2023-12-06T22:13:10Z
+
+
+ 2023-12-06T22:13:14Z
+
+
+ 2023-12-06T22:13:18Z
+
+
+ 2023-12-06T22:13:22Z
+
+
+ 2023-12-06T22:13:26Z
+
+
+ 2023-12-06T22:13:29Z
+
+
+ 2023-12-06T22:13:33Z
+
+
+ 2023-12-06T22:13:37Z
+
+
+ 2023-12-06T22:13:41Z
+
+
+ 2023-12-06T22:13:45Z
+
+
+ 2023-12-06T22:13:48Z
+
+
+ 2023-12-06T22:13:53Z
+
+
+ 2023-12-06T22:13:56Z
+
+
+ 2023-12-06T22:14:00Z
+
+
+ 2023-12-06T22:14:04Z
+
+
+ 2023-12-06T22:14:07Z
+
+
+ 2023-12-06T22:14:10Z
+
+
+ 2023-12-06T22:14:14Z
+
+
+ 2023-12-06T22:14:17Z
+
+
+ 2023-12-06T22:14:20Z
+
+
+ 2023-12-06T22:14:24Z
+
+
+ 2023-12-06T22:14:28Z
+
+
+ 2023-12-06T22:14:32Z
+
+
+ 2023-12-06T22:14:35Z
+
+
+ 2023-12-06T22:14:38Z
+
+
+ 2023-12-06T22:14:42Z
+
+
+ 2023-12-06T22:14:45Z
+
+
+ 2023-12-06T22:14:48Z
+
+
+ 2023-12-06T22:14:51Z
+
+
+ 2023-12-06T22:14:55Z
+
+
+ 2023-12-06T22:14:58Z
+
+
+ 2023-12-06T22:15:02Z
+
+
+ 2023-12-06T22:15:06Z
+
+
+ 2023-12-06T22:15:09Z
+
+
+ 2023-12-06T22:15:14Z
+
+
+ 2023-12-06T22:15:17Z
+
+
+ 2023-12-06T22:15:21Z
+
+
+ 2023-12-06T22:15:25Z
+
+
+ 2023-12-06T22:15:29Z
+
+
+ 2023-12-06T22:15:32Z
+
+
+ 2023-12-06T22:15:36Z
+
+
+ 2023-12-06T22:15:39Z
+
+
+ 2023-12-06T22:15:43Z
+
+
+ 2023-12-06T22:15:47Z
+
+
+ 2023-12-06T22:15:50Z
+
+
+ 2023-12-06T22:15:53Z
+
+
+ 2023-12-06T22:15:57Z
+
+
+ 2023-12-06T22:16:00Z
+
+
+ 2023-12-06T22:16:04Z
+
+
+ 2023-12-06T22:16:08Z
+
+
+ 2023-12-06T22:16:12Z
+
+
+ 2023-12-06T22:16:15Z
+
+
+ 2023-12-06T22:16:19Z
+
+
+ 2023-12-06T22:16:22Z
+
+
+ 2023-12-06T22:16:25Z
+
+
+ 2023-12-06T22:16:29Z
+
+
+ 2023-12-06T22:16:33Z
+
+
+ 2023-12-06T22:16:37Z
+
+
+ 2023-12-06T22:16:40Z
+
+
+ 2023-12-06T22:16:44Z
+
+
+ 2023-12-06T22:16:47Z
+
+
+ 2023-12-06T22:16:50Z
+
+
+ 2023-12-06T22:16:53Z
+
+
+ 2023-12-06T22:16:58Z
+
+
+ 2023-12-06T22:17:01Z
+
+
+ 2023-12-06T22:17:05Z
+
+
+ 2023-12-06T22:17:08Z
+
+
+ 2023-12-06T22:17:11Z
+
+
+ 2023-12-06T22:17:15Z
+
+
+ 2023-12-06T22:17:18Z
+
+
+ 2023-12-06T22:17:21Z
+
+
+ 2023-12-06T22:17:25Z
+
+
+ 2023-12-06T22:17:29Z
+
+
+ 2023-12-06T22:17:33Z
+
+
+ 2023-12-06T22:17:36Z
+
+
+ 2023-12-06T22:17:40Z
+
+
+ 2023-12-06T22:17:43Z
+
+
+ 2023-12-06T22:17:46Z
+
+
+ 2023-12-06T22:17:50Z
+
+
+ 2023-12-06T22:17:54Z
+
+
+ 2023-12-06T22:17:58Z
+
+
+ 2023-12-06T22:18:01Z
+
+
+ 2023-12-06T22:18:05Z
+
+
+ 2023-12-06T22:18:08Z
+
+
+ 2023-12-06T22:18:11Z
+
+
+ 2023-12-06T22:18:15Z
+
+
+ 2023-12-06T22:18:19Z
+
+
+ 2023-12-06T22:18:23Z
+
+
+ 2023-12-06T22:18:26Z
+
+
+ 2023-12-06T22:18:30Z
+
+
+ 2023-12-06T22:18:34Z
+
+
+ 2023-12-06T22:18:38Z
+
+
+ 2023-12-06T22:18:41Z
+
+
+ 2023-12-06T22:18:45Z
+
+
+ 2023-12-06T22:18:48Z
+
+
+ 2023-12-06T22:18:51Z
+
+
+ 2023-12-06T22:18:54Z
+
+
+ 2023-12-06T22:18:58Z
+
+
+ 2023-12-06T22:19:03Z
+
+
+ 2023-12-06T22:19:06Z
+
+
+ 2023-12-06T22:19:09Z
+
+
+ 2023-12-06T22:19:12Z
+
+
+ 2023-12-06T22:19:16Z
+
+
+ 2023-12-06T22:19:19Z
+
+
+ 2023-12-06T22:19:24Z
+
+
+ 2023-12-06T22:19:27Z
+
+
+ 2023-12-06T22:19:31Z
+
+
+ 2023-12-06T22:19:35Z
+
+
+ 2023-12-06T22:19:39Z
+
+
+ 2023-12-06T22:19:42Z
+
+
+ 2023-12-06T22:19:46Z
+
+
+ 2023-12-06T22:19:50Z
+
+
+ 2023-12-06T22:19:54Z
+
+
+ 2023-12-06T22:19:57Z
+
+
+ 2023-12-06T22:20:01Z
+
+
+ 2023-12-06T22:20:04Z
+
+
+ 2023-12-06T22:20:08Z
+
+
+ 2023-12-06T22:20:12Z
+
+
+ 2023-12-06T22:20:16Z
+
+
+ 2023-12-06T22:20:19Z
+
+
+ 2023-12-06T22:20:22Z
+
+
+ 2023-12-06T22:20:26Z
+
+
+ 2023-12-06T22:20:30Z
+
+
+ 2023-12-06T22:20:34Z
+
+
+ 2023-12-06T22:20:37Z
+
+
+ 2023-12-06T22:20:41Z
+
+
+ 2023-12-06T22:20:44Z
+
+
+ 2023-12-06T22:20:47Z
+
+
+ 2023-12-06T22:20:51Z
+
+
+ 2023-12-06T22:20:54Z
+
+
+ 2023-12-06T22:20:57Z
+
+
+ 2023-12-06T22:21:01Z
+
+
+ 2023-12-06T22:21:04Z
+
+
+ 2023-12-06T22:21:07Z
+
+
+ 2023-12-06T22:21:11Z
+
+
+ 2023-12-06T22:21:14Z
+
+
+ 2023-12-06T22:21:17Z
+
+
+ 2023-12-06T22:21:21Z
+
+
+ 2023-12-06T22:21:25Z
+
+
+ 2023-12-06T22:21:28Z
+
+
+ 2023-12-06T22:21:32Z
+
+
+ 2023-12-06T22:21:35Z
+
+
+ 2023-12-06T22:21:40Z
+
+
+ 2023-12-06T22:21:43Z
+
+
+ 2023-12-06T22:21:47Z
+
+
+ 2023-12-06T22:21:51Z
+
+
+ 2023-12-06T22:21:55Z
+
+
+ 2023-12-06T22:21:58Z
+
+
+ 2023-12-06T22:22:02Z
+
+
+ 2023-12-06T22:22:06Z
+
+
+ 2023-12-06T22:22:10Z
+
+
+ 2023-12-06T22:22:13Z
+
+
+ 2023-12-06T22:22:17Z
+
+
+ 2023-12-06T22:22:20Z
+
+
+ 2023-12-06T22:22:23Z
+
+
+ 2023-12-06T22:22:27Z
+
+
+ 2023-12-06T22:22:30Z
+
+
+ 2023-12-06T22:22:33Z
+
+
+ 2023-12-06T22:22:37Z
+
+
+ 2023-12-06T22:22:41Z
+
+
+ 2023-12-06T22:22:45Z
+
+
+ 2023-12-06T22:22:48Z
+
+
+ 2023-12-06T22:22:52Z
+
+
+ 2023-12-06T22:22:56Z
+
+
+ 2023-12-06T22:23:00Z
+
+
+ 2023-12-06T22:23:03Z
+
+
+ 2023-12-06T22:23:07Z
+
+
+ 2023-12-06T22:23:10Z
+
+
+ 2023-12-06T22:23:13Z
+
+
+ 2023-12-06T22:23:17Z
+
+
+ 2023-12-06T22:23:21Z
+
+
+ 2023-12-06T22:23:25Z
+
+
+ 2023-12-06T22:23:28Z
+
+
+ 2023-12-06T22:23:32Z
+
+
+ 2023-12-06T22:23:35Z
+
+
+ 2023-12-06T22:23:38Z
+
+
+ 2023-12-06T22:23:42Z
+
+
+ 2023-12-06T22:23:46Z
+
+
+ 2023-12-06T22:23:50Z
+
+
+ 2023-12-06T22:23:53Z
+
+
+ 2023-12-06T22:23:57Z
+
+
+ 2023-12-06T22:24:00Z
+
+
+ 2023-12-06T22:24:03Z
+
+
+ 2023-12-06T22:24:07Z
+
+
+ 2023-12-06T22:24:11Z
+
+
+ 2023-12-06T22:24:15Z
+
+
+ 2023-12-06T22:24:19Z
+
+
+ 2023-12-06T22:24:22Z
+
+
+ 2023-12-06T22:24:26Z
+
+
+ 2023-12-06T22:24:29Z
+
+
+ 2023-12-06T22:24:34Z
+
+
+ 2023-12-06T22:24:37Z
+
+
+ 2023-12-06T22:24:41Z
+
+
+ 2023-12-06T22:24:44Z
+
+
+ 2023-12-06T22:24:48Z
+
+
+ 2023-12-06T22:24:51Z
+
+
+ 2023-12-06T22:24:55Z
+
+
+ 2023-12-06T22:24:58Z
+
+
+ 2023-12-06T22:25:01Z
+
+
+ 2023-12-06T22:25:05Z
+
+
+ 2023-12-06T22:25:09Z
+
+
+ 2023-12-06T22:25:13Z
+
+
+ 2023-12-06T22:25:17Z
+
+
+ 2023-12-06T22:25:21Z
+
+
+ 2023-12-06T22:25:24Z
+
+
+ 2023-12-06T22:25:27Z
+
+
+ 2023-12-06T22:25:31Z
+
+
+ 2023-12-06T22:25:35Z
+
+
+ 2023-12-06T22:25:38Z
+
+
+ 2023-12-06T22:25:42Z
+
+
+ 2023-12-06T22:25:46Z
+
+
+ 2023-12-06T22:25:49Z
+
+
+ 2023-12-06T22:25:52Z
+
+
+ 2023-12-06T22:25:56Z
+
+
+ 2023-12-06T22:25:59Z
+
+
+ 2023-12-06T22:26:03Z
+
+
+ 2023-12-06T22:26:06Z
+
+
+ 2023-12-06T22:26:10Z
+
+
+ 2023-12-06T22:26:14Z
+
+
+ 2023-12-06T22:26:18Z
+
+
+ 2023-12-06T22:26:22Z
+
+
+ 2023-12-06T22:26:25Z
+
+
+ 2023-12-06T22:26:29Z
+
+
+ 2023-12-06T22:26:32Z
+
+
+ 2023-12-06T22:26:36Z
+
+
+ 2023-12-06T22:26:40Z
+
+
+ 2023-12-06T22:26:44Z
+
+
+ 2023-12-06T22:26:48Z
+
+
+ 2023-12-06T22:26:52Z
+
+
+ 2023-12-06T22:26:56Z
+
+
+ 2023-12-06T22:27:00Z
+
+
+ 2023-12-06T22:27:03Z
+
+
+ 2023-12-06T22:27:07Z
+
+
+ 2023-12-06T22:27:11Z
+
+
+ 2023-12-06T22:27:14Z
+
+
+ 2023-12-06T22:27:18Z
+
+
+ 2023-12-06T22:27:22Z
+
+
+ 2023-12-06T22:27:26Z
+
+
+ 2023-12-06T22:27:30Z
+
+
+ 2023-12-06T22:27:33Z
+
+
+ 2023-12-06T22:27:37Z
+
+
+ 2023-12-06T22:27:40Z
+
+
+ 2023-12-06T22:27:43Z
+
+
+ 2023-12-06T22:27:47Z
+
+
+ 2023-12-06T22:27:51Z
+
+
+ 2023-12-06T22:27:54Z
+
+
+ 2023-12-06T22:27:58Z
+
+
+ 2023-12-06T22:28:03Z
+
+
+ 2023-12-06T22:28:06Z
+
+
+ 2023-12-06T22:28:09Z
+
+
+ 2023-12-06T22:28:12Z
+
+
+ 2023-12-06T22:28:15Z
+
+
+ 2023-12-06T22:28:18Z
+
+
+ 2023-12-06T22:28:22Z
+
+
+ 2023-12-06T22:28:26Z
+
+
+ 2023-12-06T22:28:29Z
+
+
+ 2023-12-06T22:28:33Z
+
+
+ 2023-12-06T22:28:36Z
+
+
+ 2023-12-06T22:28:39Z
+
+
+ 2023-12-06T22:28:43Z
+
+
+ 2023-12-06T22:28:46Z
+
+
+ 2023-12-06T22:28:49Z
+
+
+ 2023-12-06T22:28:53Z
+
+
+ 2023-12-06T22:28:57Z
+
+
+ 2023-12-06T22:29:01Z
+
+
+ 2023-12-06T22:29:04Z
+
+
+ 2023-12-06T22:29:08Z
+
+
+ 2023-12-06T22:29:11Z
+
+
+ 2023-12-06T22:29:14Z
+
+
+ 2023-12-06T22:29:18Z
+
+
+ 2023-12-06T22:29:22Z
+
+
+ 2023-12-06T22:29:26Z
+
+
+ 2023-12-06T22:29:29Z
+
+
+ 2023-12-06T22:29:33Z
+
+
+ 2023-12-06T22:29:37Z
+
+
+ 2023-12-06T22:29:41Z
+
+
+ 2023-12-06T22:29:44Z
+
+
+ 2023-12-06T22:29:48Z
+
+
+ 2023-12-06T22:29:51Z
+
+
+ 2023-12-06T22:29:54Z
+
+
+ 2023-12-06T22:29:58Z
+
+
+ 2023-12-06T22:30:10Z
+
+
+ 2023-12-06T22:30:13Z
+
+
+ 2023-12-06T22:30:44Z
+
+
+ 2023-12-06T22:30:48Z
+
+
+ 2023-12-06T22:30:51Z
+
+
+ 2023-12-06T22:30:55Z
+
+
+ 2023-12-06T22:30:58Z
+
+
+ 2023-12-06T22:31:02Z
+
+
+ 2023-12-06T22:31:05Z
+
+
+ 2023-12-06T22:31:08Z
+
+
+ 2023-12-06T22:31:12Z
+
+
+ 2023-12-06T22:31:16Z
+
+
+ 2023-12-06T22:31:20Z
+
+
+ 2023-12-06T22:31:24Z
+
+
+ 2023-12-06T22:31:28Z
+
+
+ 2023-12-06T22:31:32Z
+
+
+ 2023-12-06T22:31:36Z
+
+
+ 2023-12-06T22:31:39Z
+
+
+ 2023-12-06T22:31:43Z
+
+
+ 2023-12-06T22:31:55Z
+
+
+ 2023-12-06T22:32:16Z
+
+
+ 2023-12-06T22:32:19Z
+
+
+ 2023-12-06T22:32:23Z
+
+
+ 2023-12-06T22:33:00Z
+
+
+ 2023-12-06T22:33:03Z
+
+
+ 2023-12-06T22:33:07Z
+
+
+ 2023-12-06T22:33:51Z
+
+
+ 2023-12-06T22:33:54Z
+
+
+ 2023-12-06T22:34:05Z
+
+
+ 2023-12-06T22:34:08Z
+
+
+ 2023-12-06T22:34:12Z
+
+
+ 2023-12-06T22:34:15Z
+
+
+ 2023-12-06T22:34:19Z
+
+
+ 2023-12-06T22:34:22Z
+
+
+ 2023-12-06T22:34:26Z
+
+
+
+
+ 2023_10_09T09_34_59.283419Z.gpx
+ Routes from sunnypilot 0.9.4.1-release (TOYOTA HIGHLANDER HYBRID 2020).
+ /user/sunnypilot/traces/10907732
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:14Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:15Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:16Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:17Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:18Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:19Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:20Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:21Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:22Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:23Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:24Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:25Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:26Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:27Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:28Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:29Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:30Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:31Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:32Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:33Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:34Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:35Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:36Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:37Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:38Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:39Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:40Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:41Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:42Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:43Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:44Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:45Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:46Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:47Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:48Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:49Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:50Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:51Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:52Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:53Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:54Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:55Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:56Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:57Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:58Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:36:59Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:00Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:01Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:02Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:03Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:04Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:05Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:06Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:07Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:08Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:09Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:10Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:11Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:12Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:13Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:14Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:15Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:16Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:17Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:18Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:19Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:20Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:21Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:22Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:23Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:24Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:25Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:26Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:27Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:28Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:29Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:30Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:31Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:32Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:33Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:34Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:35Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:36Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:37Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:38Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:39Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:40Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:41Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:42Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:43Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:44Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:45Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:46Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:47Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:48Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:49Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:50Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:51Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:52Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:53Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:54Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:55Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:56Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:57Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:58Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:37:59Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:00Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:01Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:02Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:03Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:04Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:05Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:06Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:07Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:08Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:09Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:10Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:11Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:12Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:13Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:14Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:15Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:16Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:17Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:18Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:19Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:20Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:21Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:22Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:23Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:24Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:25Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:26Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:27Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:28Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:29Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:30Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:31Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:32Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:33Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:34Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:35Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:36Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:37Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:38Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:39Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:40Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:41Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:42Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:43Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:44Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:45Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:46Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:47Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:48Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:49Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:50Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:51Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:52Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:53Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:54Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:55Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:56Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:57Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:58Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:38:59Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:00Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:01Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:02Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:03Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:04Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:05Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:06Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:07Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:08Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:09Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:10Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:11Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:12Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:13Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:14Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:15Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:16Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:17Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:18Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:19Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:20Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:21Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:22Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:23Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:24Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:25Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:26Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:27Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:28Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:29Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:30Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:31Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:32Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:33Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:34Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:35Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:36Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:37Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:38Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:39Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:40Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:41Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:42Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:43Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:44Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:45Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:46Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:47Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:48Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:49Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:50Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:51Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:52Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:53Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:54Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:55Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:56Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:57Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:58Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:39:59Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:00Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:01Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:02Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:03Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:04Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:05Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:06Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:07Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:08Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:09Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:10Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:11Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:12Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:13Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:14Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:15Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:16Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:17Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:18Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:19Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:20Z
+
+
+ 2023-10-09T09:40:21Z
+
+
+ 2023-10-09T09:40:21Z
+
+
+ 2023-10-09T09:40:21Z
+
+
+ 2023-10-09T09:40:21Z
+
+
+ 2023-10-09T09:40:21Z
+
+
+
+
+ swietokrzyska_droga_sw_jakuba.gpx
+ Świętokrzyska Droga św. Jakuba
+ /user/europa_caminonetpl/traces/8735777
+
+
+ 2023-07-16T07:15:08Z
+
+
+ 2023-07-16T07:15:28Z
+
+
+ 2023-07-16T07:15:42Z
+
+
+ 2023-07-16T07:15:45Z
+
+
+ 2023-07-16T07:15:49Z
+
+
+ 2023-07-16T07:15:55Z
+
+
+ 2023-07-16T07:15:58Z
+
+
+ 2023-07-16T07:16:01Z
+
+
+ 2023-07-16T07:16:03Z
+
+
+ 2023-07-16T07:16:10Z
+
+
+ 2023-07-16T07:16:12Z
+
+
+ 2023-07-16T07:16:14Z
+
+
+ 2023-07-16T07:16:17Z
+
+
+ 2023-07-16T07:16:21Z
+
+
+ 2023-07-16T07:16:40Z
+
+
+ 2023-07-16T07:16:52Z
+
+
+ 2023-07-16T07:16:54Z
+
+
+ 2023-07-16T07:16:56Z
+
+
+ 2023-07-16T07:17:08Z
+
+
+ 2023-07-16T07:17:48Z
+
+
+ 2023-07-16T07:17:48Z
+
+
+ 2023-07-16T07:17:57Z
+
+
+ 2023-07-16T07:18:02Z
+
+
+ 2023-07-16T07:18:03Z
+
+
+ 2023-07-16T07:18:07Z
+
+
+ 2023-07-16T07:18:10Z
+
+
+ 2023-07-16T07:18:13Z
+
+
+ 2023-07-16T07:18:31Z
+
+
+ 2023-07-16T07:18:50Z
+
+
+ 2023-07-16T07:18:51Z
+
+
+ 2023-07-16T07:18:53Z
+
+
+ 2023-07-16T07:18:58Z
+
+
+ 2023-07-16T07:18:59Z
+
+
+ 2023-07-16T07:19:01Z
+
+
+ 2023-07-16T07:19:06Z
+
+
+ 2023-07-16T07:19:10Z
+
+
+ 2023-07-16T07:19:30Z
+
+
+ 2023-07-16T07:19:48Z
+
+
+ 2023-07-16T07:20:17Z
+
+
+ 2023-07-16T07:20:18Z
+
+
+ 2023-07-16T07:20:21Z
+
+
+ 2023-07-16T07:20:24Z
+
+
+ 2023-07-16T07:20:24Z
+
+
+ 2023-07-16T07:20:30Z
+
+
+ 2023-07-16T07:20:31Z
+
+
+ 2023-07-16T07:20:54Z
+
+
+ 2023-07-16T07:20:57Z
+
+
+ 2023-07-16T07:20:59Z
+
+
+ 2023-07-16T07:21:00Z
+
+
+ 2023-07-16T07:21:02Z
+
+
+ 2023-07-16T07:21:04Z
+
+
+ 2023-07-16T07:21:06Z
+
+
+ 2023-07-16T07:21:07Z
+
+
+ 2023-07-16T07:22:28Z
+
+
+ 2023-07-16T07:22:45Z
+
+
+ 2023-07-16T07:22:56Z
+
+
+ 2023-07-16T07:23:03Z
+
+
+ 2023-07-16T07:23:05Z
+
+
+ 2023-07-16T07:23:11Z
+
+
+ 2023-07-16T07:23:12Z
+
+
+ 2023-07-16T07:23:14Z
+
+
+ 2023-07-16T07:23:15Z
+
+
+ 2023-07-16T07:23:23Z
+
+
+ 2023-07-16T07:23:24Z
+
+
+ 2023-07-16T07:23:26Z
+
+
+ 2023-07-16T07:23:28Z
+
+
+ 2023-07-16T07:23:30Z
+
+
+ 2023-07-16T07:23:32Z
+
+
+ 2023-07-16T07:23:34Z
+
+
+ 2023-07-16T07:23:38Z
+
+
+ 2023-07-16T07:23:41Z
+
+
+ 2023-07-16T07:23:51Z
+
+
+ 2023-07-16T07:25:16Z
+
+
+ 2023-07-16T07:25:21Z
+
+
+ 2023-07-16T07:25:29Z
+
+
+ 2023-07-16T07:25:34Z
+
+
+ 2023-07-16T07:25:36Z
+
+
+ 2023-07-16T07:25:43Z
+
+
+ 2023-07-16T07:25:47Z
+
+
+ 2023-07-16T07:25:49Z
+
+
+ 2023-07-16T07:25:55Z
+
+
+ 2023-07-16T07:26:00Z
+
+
+ 2023-07-16T07:26:03Z
+
+
+ 2023-07-16T07:26:09Z
+
+
+ 2023-07-16T07:26:10Z
+
+
+ 2023-07-16T07:26:14Z
+
+
+ 2023-07-16T07:26:16Z
+
+
+ 2023-07-16T07:26:23Z
+
+
+ 2023-07-16T07:26:25Z
+
+
+ 2023-07-16T07:26:27Z
+
+
+ 2023-07-16T07:26:28Z
+
+
+ 2023-07-16T07:26:30Z
+
+
+ 2023-07-16T07:26:31Z
+
+
+ 2023-07-16T07:26:34Z
+
+
+ 2023-07-16T07:26:50Z
+
+
+ 2023-07-16T07:26:52Z
+
+
+ 2023-07-16T07:26:55Z
+
+
+ 2023-07-16T07:27:26Z
+
+
+ 2023-07-16T07:27:33Z
+
+
+ 2023-07-16T07:27:36Z
+
+
+ 2023-07-16T07:27:39Z
+
+
+ 2023-07-16T07:27:43Z
+
+
+ 2023-07-16T07:27:48Z
+
+
+ 2023-07-16T07:27:55Z
+
+
+ 2023-07-16T07:27:59Z
+
+
+ 2023-07-16T07:28:04Z
+
+
+ 2023-07-16T07:28:14Z
+
+
+ 2023-07-16T07:28:18Z
+
+
+ 2023-07-16T07:28:21Z
+
+
+ 2023-07-16T07:28:24Z
+
+
+ 2023-07-16T07:28:29Z
+
+
+ 2023-07-16T07:28:41Z
+
+
+ 2023-07-16T07:28:53Z
+
+
+ 2023-07-16T07:29:25Z
+
+
+ 2023-07-16T07:29:30Z
+
+
+ 2023-07-16T07:29:34Z
+
+
+ 2023-07-16T07:29:50Z
+
+
+ 2023-07-16T07:29:53Z
+
+
+ 2023-07-16T07:29:55Z
+
+
+ 2023-07-16T07:30:10Z
+
+
+ 2023-07-16T07:30:14Z
+
+
+ 2023-07-16T07:30:21Z
+
+
+ 2023-07-16T07:30:26Z
+
+
+ 2023-07-16T07:30:34Z
+
+
+ 2023-07-16T07:30:36Z
+
+
+ 2023-07-16T07:30:46Z
+
+
+ 2023-07-16T07:30:48Z
+
+
+ 2023-07-16T07:30:49Z
+
+
+ 2023-07-16T07:30:52Z
+
+
+ 2023-07-16T07:30:58Z
+
+
+ 2023-07-16T07:31:03Z
+
+
+ 2023-07-16T07:31:08Z
+
+
+ 2023-07-16T07:31:09Z
+
+
+ 2023-07-16T07:31:11Z
+
+
+ 2023-07-16T07:31:34Z
+
+
+ 2023-07-16T07:31:35Z
+
+
+ 2023-07-16T07:31:37Z
+
+
+ 2023-07-16T07:31:38Z
+
+
+ 2023-07-16T07:31:39Z
+
+
+ 2023-07-16T07:31:41Z
+
+
+ 2023-07-16T07:31:56Z
+
+
+ 2023-07-16T07:31:57Z
+
+
+ 2023-07-16T07:31:59Z
+
+
+ 2023-07-16T07:32:01Z
+
+
+ 2023-07-16T07:32:16Z
+
+
+ 2023-07-16T07:32:26Z
+
+
+ 2023-07-16T07:32:31Z
+
+
+ 2023-07-16T07:33:07Z
+
+
+ 2023-07-16T07:33:55Z
+
+
+ 2023-07-16T07:33:58Z
+
+
+ 2023-07-16T07:34:35Z
+
+
+ 2023-07-16T07:35:56Z
+
+
+ 2023-07-16T07:36:01Z
+
+
+ 2023-07-16T07:36:22Z
+
+
+ 2023-07-16T07:37:23Z
+
+
+ 2023-07-16T07:37:40Z
+
+
+ 2023-07-16T07:37:44Z
+
+
+ 2023-07-16T07:37:53Z
+
+
+ 2023-07-16T07:37:56Z
+
+
+ 2023-07-16T07:38:02Z
+
+
+ 2023-07-16T07:38:07Z
+
+
+ 2023-07-16T07:38:11Z
+
+
+ 2023-07-16T07:38:14Z
+
+
+ 2023-07-16T07:38:15Z
+
+
+ 2023-07-16T07:38:16Z
+
+
+ 2023-07-16T07:38:17Z
+
+
+ 2023-07-16T07:38:26Z
+
+
+ 2023-07-16T07:38:29Z
+
+
+ 2023-07-16T07:38:33Z
+
+
+ 2023-07-16T07:38:36Z
+
+
+ 2023-07-16T07:38:48Z
+
+
+ 2023-07-16T07:39:01Z
+
+
+ 2023-07-16T07:39:56Z
+
+
+ 2023-07-16T07:40:03Z
+
+
+ 2023-07-16T07:40:23Z
+
+
+
+
+ 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-25T14:50:28Z
+
+
+ 2023-06-25T14:50:32Z
+
+
+ 2023-06-25T14:50:33Z
+
+
+ 2023-06-25T14:50:35Z
+
+
+ 2023-06-25T14:50:36Z
+
+
+ 2023-06-25T14:50:37Z
+
+
+ 2023-06-25T14:50:38Z
+
+
+ 2023-06-25T14:50:39Z
+
+
+ 2023-06-25T14:50:40Z
+
+
+ 2023-06-25T14:50:41Z
+
+
+ 2023-06-25T14:50:42Z
+
+
+ 2023-06-25T14:50:43Z
+
+
+ 2023-06-25T14:50:44Z
+
+
+ 2023-06-25T14:50:45Z
+
+
+ 2023-06-25T14:50:46Z
+
+
+
+
+ 2023_06_08T09_45_48.530193Z.gpx
+ Routes from sunnypilot 2022.11.13 (TOYOTA HIGHLANDER HYBRID 2020).
+ /user/sunnypilot/traces/7870041
+
+
+ 2023-06-08T09:46:59Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:00Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:01Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:02Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:03Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:04Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:05Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:06Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:07Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:08Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:09Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:10Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:11Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:12Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:13Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:14Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:15Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:16Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:17Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:18Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:19Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:20Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:21Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:22Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+ 2023-06-08T09:47:23Z
+
+
+
+
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