mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +198 -55
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +10 -6
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +18 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +182 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +411 -387
- mapillary_tools/upload_api_v4.py +167 -142
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
mapillary_tools/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.
|
|
1
|
+
VERSION = "0.14.0"
|
mapillary_tools/api_v4.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
1
4
|
import logging
|
|
2
5
|
import os
|
|
3
6
|
import ssl
|
|
@@ -18,6 +21,24 @@ REQUESTS_TIMEOUT = 60 # 1 minutes
|
|
|
18
21
|
USE_SYSTEM_CERTS: bool = False
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
class HTTPContentError(Exception):
|
|
25
|
+
"""
|
|
26
|
+
Raised when the HTTP response is ok (200) but the content is not as expected
|
|
27
|
+
e.g. not JSON or not a valid response.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, response: requests.Response):
|
|
31
|
+
self.response = response
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClusterFileType(enum.Enum):
|
|
36
|
+
ZIP = "zip"
|
|
37
|
+
BLACKVUE = "mly_blackvue_video"
|
|
38
|
+
CAMM = "mly_camm_video"
|
|
39
|
+
MLY_BUNDLE_MANIFEST = "mly_bundle_manifest"
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
class HTTPSystemCertsAdapter(HTTPAdapter):
|
|
22
43
|
"""
|
|
23
44
|
This adapter uses the system's certificate store instead of the certifi module.
|
|
@@ -48,29 +69,30 @@ class HTTPSystemCertsAdapter(HTTPAdapter):
|
|
|
48
69
|
|
|
49
70
|
|
|
50
71
|
@T.overload
|
|
51
|
-
def _truncate(s: bytes, limit: int =
|
|
72
|
+
def _truncate(s: bytes, limit: int = 256) -> bytes | str: ...
|
|
52
73
|
|
|
53
74
|
|
|
54
75
|
@T.overload
|
|
55
|
-
def _truncate(s: str, limit: int =
|
|
76
|
+
def _truncate(s: str, limit: int = 256) -> str: ...
|
|
56
77
|
|
|
57
78
|
|
|
58
|
-
def _truncate(s, limit=
|
|
79
|
+
def _truncate(s, limit=256):
|
|
59
80
|
if limit < len(s):
|
|
81
|
+
if isinstance(s, bytes):
|
|
82
|
+
try:
|
|
83
|
+
s = s.decode("utf-8")
|
|
84
|
+
except UnicodeDecodeError:
|
|
85
|
+
pass
|
|
60
86
|
remaining = len(s) - limit
|
|
61
87
|
if isinstance(s, bytes):
|
|
62
|
-
return (
|
|
63
|
-
s[:limit]
|
|
64
|
-
+ b"..."
|
|
65
|
-
+ f"({remaining} more bytes truncated)".encode("utf-8")
|
|
66
|
-
)
|
|
88
|
+
return s[:limit] + f"...({remaining} bytes truncated)".encode("utf-8")
|
|
67
89
|
else:
|
|
68
|
-
return str(s[:limit]) + f"...({remaining}
|
|
90
|
+
return str(s[:limit]) + f"...({remaining} chars truncated)"
|
|
69
91
|
else:
|
|
70
92
|
return s
|
|
71
93
|
|
|
72
94
|
|
|
73
|
-
def _sanitize(headers: T.
|
|
95
|
+
def _sanitize(headers: T.Mapping[T.Any, T.Any]) -> T.Mapping[T.Any, T.Any]:
|
|
74
96
|
new_headers = {}
|
|
75
97
|
|
|
76
98
|
for k, v in headers.items():
|
|
@@ -81,10 +103,14 @@ def _sanitize(headers: T.Dict):
|
|
|
81
103
|
"access-token",
|
|
82
104
|
"access_token",
|
|
83
105
|
"password",
|
|
106
|
+
"user_upload_token",
|
|
84
107
|
]:
|
|
85
108
|
new_headers[k] = "[REDACTED]"
|
|
86
109
|
else:
|
|
87
|
-
|
|
110
|
+
if isinstance(v, (str, bytes)):
|
|
111
|
+
new_headers[k] = T.cast(T.Any, _truncate(v))
|
|
112
|
+
else:
|
|
113
|
+
new_headers[k] = v
|
|
88
114
|
|
|
89
115
|
return new_headers
|
|
90
116
|
|
|
@@ -92,9 +118,9 @@ def _sanitize(headers: T.Dict):
|
|
|
92
118
|
def _log_debug_request(
|
|
93
119
|
method: str,
|
|
94
120
|
url: str,
|
|
95
|
-
json:
|
|
96
|
-
params:
|
|
97
|
-
headers:
|
|
121
|
+
json: dict | None = None,
|
|
122
|
+
params: dict | None = None,
|
|
123
|
+
headers: dict | None = None,
|
|
98
124
|
timeout: T.Any = None,
|
|
99
125
|
):
|
|
100
126
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
@@ -118,6 +144,8 @@ def _log_debug_request(
|
|
|
118
144
|
if timeout is not None:
|
|
119
145
|
msg += f" TIMEOUT={timeout}"
|
|
120
146
|
|
|
147
|
+
msg = msg.replace("\n", "\\n")
|
|
148
|
+
|
|
121
149
|
LOG.debug(msg)
|
|
122
150
|
|
|
123
151
|
|
|
@@ -125,44 +153,61 @@ def _log_debug_response(resp: requests.Response):
|
|
|
125
153
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
126
154
|
return
|
|
127
155
|
|
|
128
|
-
|
|
156
|
+
elapsed = resp.elapsed.total_seconds() * 1000 # Convert to milliseconds
|
|
157
|
+
msg = f"HTTP {resp.status_code} {resp.reason} ({elapsed:.0f} ms): {str(_truncate_response_content(resp))}"
|
|
158
|
+
|
|
159
|
+
LOG.debug(msg)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _truncate_response_content(resp: requests.Response) -> str | bytes:
|
|
129
163
|
try:
|
|
130
|
-
|
|
131
|
-
except
|
|
132
|
-
|
|
164
|
+
json_data = resp.json()
|
|
165
|
+
except requests.JSONDecodeError:
|
|
166
|
+
if resp.content is not None:
|
|
167
|
+
data = _truncate(resp.content)
|
|
168
|
+
else:
|
|
169
|
+
data = ""
|
|
170
|
+
else:
|
|
171
|
+
if isinstance(json_data, dict):
|
|
172
|
+
data = _truncate(dumps(_sanitize(json_data)))
|
|
173
|
+
else:
|
|
174
|
+
data = _truncate(str(json_data))
|
|
175
|
+
|
|
176
|
+
if isinstance(data, bytes):
|
|
177
|
+
return data.replace(b"\n", b"\\n")
|
|
133
178
|
|
|
134
|
-
|
|
179
|
+
elif isinstance(data, str):
|
|
180
|
+
return data.replace("\n", "\\n")
|
|
181
|
+
|
|
182
|
+
return data
|
|
135
183
|
|
|
136
184
|
|
|
137
185
|
def readable_http_error(ex: requests.HTTPError) -> str:
|
|
138
|
-
|
|
139
|
-
resp = ex.response
|
|
186
|
+
return readable_http_response(ex.response)
|
|
140
187
|
|
|
141
|
-
data: T.Union[str, bytes]
|
|
142
|
-
try:
|
|
143
|
-
data = _truncate(dumps(_sanitize(resp.json())))
|
|
144
|
-
except Exception:
|
|
145
|
-
data = _truncate(resp.content)
|
|
146
188
|
|
|
147
|
-
|
|
189
|
+
def readable_http_response(resp: requests.Response) -> str:
|
|
190
|
+
return f"{resp.request.method} {resp.url} => {resp.status_code} {resp.reason}: {str(_truncate_response_content(resp))}"
|
|
148
191
|
|
|
149
192
|
|
|
150
193
|
def request_post(
|
|
151
194
|
url: str,
|
|
152
|
-
data: T.
|
|
153
|
-
json:
|
|
195
|
+
data: T.Any | None = None,
|
|
196
|
+
json: dict | None = None,
|
|
197
|
+
disable_debug=False,
|
|
154
198
|
**kwargs,
|
|
155
199
|
) -> requests.Response:
|
|
156
200
|
global USE_SYSTEM_CERTS
|
|
157
201
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
202
|
+
if not disable_debug:
|
|
203
|
+
_log_debug_request(
|
|
204
|
+
"POST",
|
|
205
|
+
url,
|
|
206
|
+
json=json,
|
|
207
|
+
params=kwargs.get("params"),
|
|
208
|
+
headers=kwargs.get("headers"),
|
|
209
|
+
timeout=kwargs.get("timeout"),
|
|
210
|
+
)
|
|
166
211
|
|
|
167
212
|
if USE_SYSTEM_CERTS:
|
|
168
213
|
with requests.Session() as session:
|
|
@@ -182,25 +227,26 @@ def request_post(
|
|
|
182
227
|
)
|
|
183
228
|
return request_post(url, data=data, json=json, **kwargs)
|
|
184
229
|
|
|
185
|
-
|
|
230
|
+
if not disable_debug:
|
|
231
|
+
_log_debug_response(resp)
|
|
186
232
|
|
|
187
233
|
return resp
|
|
188
234
|
|
|
189
235
|
|
|
190
236
|
def request_get(
|
|
191
|
-
url: str,
|
|
192
|
-
params: T.Optional[dict] = None,
|
|
193
|
-
**kwargs,
|
|
237
|
+
url: str, params: dict | None = None, disable_debug=False, **kwargs
|
|
194
238
|
) -> requests.Response:
|
|
195
239
|
global USE_SYSTEM_CERTS
|
|
196
240
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
241
|
+
if not disable_debug:
|
|
242
|
+
_log_debug_request(
|
|
243
|
+
"GET",
|
|
244
|
+
url,
|
|
245
|
+
params=kwargs.get("params"),
|
|
246
|
+
headers=kwargs.get("headers"),
|
|
247
|
+
# Do not log timeout here as it's always set to REQUESTS_TIMEOUT
|
|
248
|
+
timeout=None,
|
|
249
|
+
)
|
|
204
250
|
|
|
205
251
|
if USE_SYSTEM_CERTS:
|
|
206
252
|
with requests.Session() as session:
|
|
@@ -219,11 +265,50 @@ def request_get(
|
|
|
219
265
|
)
|
|
220
266
|
resp = request_get(url, params=params, **kwargs)
|
|
221
267
|
|
|
222
|
-
|
|
268
|
+
if not disable_debug:
|
|
269
|
+
_log_debug_response(resp)
|
|
223
270
|
|
|
224
271
|
return resp
|
|
225
272
|
|
|
226
273
|
|
|
274
|
+
def is_auth_error(resp: requests.Response) -> bool:
|
|
275
|
+
if resp.status_code in [401, 403]:
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
if resp.status_code in [400]:
|
|
279
|
+
try:
|
|
280
|
+
error_body = resp.json()
|
|
281
|
+
except Exception:
|
|
282
|
+
error_body = {}
|
|
283
|
+
|
|
284
|
+
type = error_body.get("debug_info", {}).get("type")
|
|
285
|
+
if type in ["NotAuthorizedError"]:
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def extract_auth_error_message(resp: requests.Response) -> str:
|
|
292
|
+
assert is_auth_error(resp), "has to be an auth error"
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
error_body = resp.json()
|
|
296
|
+
except Exception:
|
|
297
|
+
error_body = {}
|
|
298
|
+
|
|
299
|
+
# from Graph APIs
|
|
300
|
+
message = error_body.get("error", {}).get("message")
|
|
301
|
+
if message is not None:
|
|
302
|
+
return str(message)
|
|
303
|
+
|
|
304
|
+
# from upload service
|
|
305
|
+
message = error_body.get("debug_info", {}).get("message")
|
|
306
|
+
if message is not None:
|
|
307
|
+
return str(message)
|
|
308
|
+
|
|
309
|
+
return resp.text
|
|
310
|
+
|
|
311
|
+
|
|
227
312
|
def get_upload_token(email: str, password: str) -> requests.Response:
|
|
228
313
|
resp = request_post(
|
|
229
314
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
|
|
@@ -236,7 +321,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
|
|
|
236
321
|
|
|
237
322
|
|
|
238
323
|
def fetch_organization(
|
|
239
|
-
user_access_token: str, organization_id:
|
|
324
|
+
user_access_token: str, organization_id: int | str
|
|
240
325
|
) -> requests.Response:
|
|
241
326
|
resp = request_get(
|
|
242
327
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
|
|
@@ -252,22 +337,80 @@ def fetch_organization(
|
|
|
252
337
|
return resp
|
|
253
338
|
|
|
254
339
|
|
|
340
|
+
def fetch_user_or_me(
|
|
341
|
+
user_access_token: str, user_id: int | str | None = None
|
|
342
|
+
) -> requests.Response:
|
|
343
|
+
if user_id is None:
|
|
344
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
|
|
345
|
+
else:
|
|
346
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
|
|
347
|
+
|
|
348
|
+
resp = request_get(
|
|
349
|
+
url,
|
|
350
|
+
params={
|
|
351
|
+
"fields": ",".join(["id", "username"]),
|
|
352
|
+
},
|
|
353
|
+
headers={
|
|
354
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
355
|
+
},
|
|
356
|
+
timeout=REQUESTS_TIMEOUT,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
resp.raise_for_status()
|
|
360
|
+
return resp
|
|
361
|
+
|
|
362
|
+
|
|
255
363
|
ActionType = T.Literal[
|
|
256
364
|
"upload_started_upload", "upload_finished_upload", "upload_failed_upload"
|
|
257
365
|
]
|
|
258
366
|
|
|
259
367
|
|
|
260
|
-
def log_event(action_type: ActionType, properties:
|
|
368
|
+
def log_event(action_type: ActionType, properties: dict) -> requests.Response:
|
|
261
369
|
resp = request_post(
|
|
262
370
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
|
|
263
|
-
json={
|
|
264
|
-
"action_type": action_type,
|
|
265
|
-
"properties": properties,
|
|
266
|
-
},
|
|
371
|
+
json={"action_type": action_type, "properties": properties},
|
|
267
372
|
headers={
|
|
268
373
|
"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}",
|
|
269
374
|
},
|
|
270
375
|
timeout=REQUESTS_TIMEOUT,
|
|
376
|
+
disable_debug=True,
|
|
377
|
+
)
|
|
378
|
+
resp.raise_for_status()
|
|
379
|
+
return resp
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def finish_upload(
|
|
383
|
+
user_access_token: str,
|
|
384
|
+
file_handle: str,
|
|
385
|
+
cluster_filetype: ClusterFileType,
|
|
386
|
+
organization_id: int | str | None = None,
|
|
387
|
+
) -> requests.Response:
|
|
388
|
+
data: dict[str, str | int] = {
|
|
389
|
+
"file_handle": file_handle,
|
|
390
|
+
"file_type": cluster_filetype.value,
|
|
391
|
+
}
|
|
392
|
+
if organization_id is not None:
|
|
393
|
+
data["organization_id"] = organization_id
|
|
394
|
+
|
|
395
|
+
resp = request_post(
|
|
396
|
+
f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload",
|
|
397
|
+
headers={
|
|
398
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
399
|
+
},
|
|
400
|
+
json=data,
|
|
401
|
+
timeout=REQUESTS_TIMEOUT,
|
|
271
402
|
)
|
|
403
|
+
|
|
272
404
|
resp.raise_for_status()
|
|
405
|
+
|
|
273
406
|
return resp
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def jsonify_response(resp: requests.Response) -> T.Any:
|
|
410
|
+
"""
|
|
411
|
+
Convert the response to JSON, raising HTTPContentError if the response is not JSON.
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
return resp.json()
|
|
415
|
+
except requests.JSONDecodeError as ex:
|
|
416
|
+
raise HTTPContentError("Invalid JSON response", resp) from ex
|