|
1 | 1 | import io |
2 | | -from typing import Dict, Optional |
| 2 | +import re |
| 3 | +from typing import Dict, List, Optional, Tuple |
3 | 4 | from uuid import UUID |
4 | 5 |
|
5 | 6 | import requests |
@@ -63,32 +64,26 @@ async def save( |
63 | 64 | session_data.changeset = changeset |
64 | 65 | await backend.update(session_id, session_data) |
65 | 66 |
|
66 | | - # OsmChange |
67 | | - out = io.StringIO() |
68 | | - o = OsmSax.OsmSaxWriter(out, "UTF-8") |
69 | | - o.startDocument() |
70 | | - o.startElement("osmChange", {"version": "0.6", "generator": "OsmSax"}) |
71 | | - |
72 | | - methode = {"node": o.NodeCreate, "way": o.WayCreate, "relation": o.RelationCreate} |
| 67 | + osmchange: List[Dict] = [] |
73 | 68 | for action in ("modify", "delete"): |
74 | 69 | if action in json and len(json[action]) > 0: |
75 | | - o.startElement(action, {}) |
76 | 70 | for e in json[action]: |
77 | 71 | try: |
78 | 72 | ee = utils.fetch_osm_elem(e["type"], e["id"]) |
79 | 73 | except Exception: |
80 | 74 | ee = None |
81 | 75 | if ee and ee["version"] == int(e["version"]): |
82 | | - ee["changeset"] = changeset |
83 | | - ee["tag"] = e["tags"] |
84 | | - methode[e["type"]](ee) |
| 76 | + osmchange.append( |
| 77 | + { |
| 78 | + "action": action, |
| 79 | + "type": e["type"], |
| 80 | + "osm_elem": ee, |
| 81 | + "tags": e["tags"], |
| 82 | + } |
| 83 | + ) |
85 | 84 | else: |
86 | 85 | # FIXME reject |
87 | 86 | pass |
88 | | - o.endElement(action) |
89 | | - |
90 | | - o.endElement("osmChange") |
91 | | - osmchange = out.getvalue() |
92 | 87 |
|
93 | 88 | # Fire the changeset |
94 | 89 | _changeset_upload(session_data.oauth2_token, changeset, osmchange) |
@@ -143,13 +138,71 @@ def _changeset_close(oauth2_token: str, id: str) -> None: |
143 | 138 | request.raise_for_status() |
144 | 139 |
|
145 | 140 |
|
146 | | -def _changeset_upload(oauth2_token: str, id: str, osmchange) -> None: |
147 | | - request = requests.post( |
148 | | - utils.remote_url_write + "api/0.6/changeset/" + id + "/upload", |
149 | | - data=osmchange.encode(), |
150 | | - headers={ |
151 | | - "Authorization": f"Bearer {oauth2_token}", |
152 | | - "Content-Type": "application/xml; charset=utf-8", |
153 | | - }, |
154 | | - ) |
155 | | - request.raise_for_status() |
| 141 | +def _build_osmchange(changeset_id: str, elements: List[Dict]) -> str: |
| 142 | + out = io.StringIO() |
| 143 | + o = OsmSax.OsmSaxWriter(out, "UTF-8") |
| 144 | + o.startDocument() |
| 145 | + o.startElement("osmChange", {"version": "0.6", "generator": "OsmSax"}) |
| 146 | + |
| 147 | + methode = {"node": o.NodeCreate, "way": o.WayCreate, "relation": o.RelationCreate} |
| 148 | + for action in ("modify", "delete"): |
| 149 | + action_elems = [el for el in elements if el["action"] == action] |
| 150 | + if action_elems: |
| 151 | + o.startElement(action, {}) |
| 152 | + for el in action_elems: |
| 153 | + ee = dict(el["osm_elem"]) |
| 154 | + ee["changeset"] = changeset_id |
| 155 | + ee["tag"] = el["tags"] |
| 156 | + methode[el["type"]](ee) |
| 157 | + o.endElement(action) |
| 158 | + |
| 159 | + o.endElement("osmChange") |
| 160 | + return out.getvalue() |
| 161 | + |
| 162 | + |
| 163 | +def _parse_conflict_element(body: str) -> Tuple[Optional[str], Optional[int]]: |
| 164 | + # Typical body: "Version mismatch: Provided 3, server had: 4 of Node 12345" |
| 165 | + match = re.search(r"of (Node|Way|Relation) (\d+)", body, re.IGNORECASE) |
| 166 | + if match: |
| 167 | + return match.group(1).lower(), int(match.group(2)) |
| 168 | + return None, None |
| 169 | + |
| 170 | + |
| 171 | +def _changeset_upload( |
| 172 | + oauth2_token: str, changeset_id: str, elements: List[Dict] |
| 173 | +) -> None: |
| 174 | + while True: # Do as much retries as we can |
| 175 | + osmchange = _build_osmchange(changeset_id, elements) |
| 176 | + response = requests.post( |
| 177 | + utils.remote_url_write + "api/0.6/changeset/" + changeset_id + "/upload", |
| 178 | + data=osmchange.encode(), |
| 179 | + headers={ |
| 180 | + "Authorization": f"Bearer {oauth2_token}", |
| 181 | + "Content-Type": "application/xml; charset=utf-8", |
| 182 | + }, |
| 183 | + ) |
| 184 | + |
| 185 | + if response.status_code == 200: |
| 186 | + break |
| 187 | + |
| 188 | + if response.status_code != 409: |
| 189 | + response.raise_for_status() |
| 190 | + |
| 191 | + elem_type, elem_id = _parse_conflict_element(response.text) |
| 192 | + |
| 193 | + if elem_type is None or elem_id is None: |
| 194 | + # Cannot parse the conflict; raise so the caller sees the error. |
| 195 | + response.raise_for_status() |
| 196 | + |
| 197 | + elements = list( |
| 198 | + filter( |
| 199 | + lambda el: not ( |
| 200 | + el["type"] == elem_type and el["osm_elem"]["id"] == elem_id |
| 201 | + ), |
| 202 | + elements, |
| 203 | + ) |
| 204 | + ) |
| 205 | + |
| 206 | + # If no elements remain there is nothing left to upload. |
| 207 | + if len(elements) == 0: |
| 208 | + break |
0 commit comments