mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a1__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.
Files changed (64) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +235 -14
  3. mapillary_tools/authenticate.py +325 -64
  4. mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +425 -177
  7. mapillary_tools/commands/__main__.py +11 -4
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +19 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +18 -9
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +28 -12
  15. mapillary_tools/constants.py +46 -4
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +158 -53
  18. mapillary_tools/exiftool_read.py +19 -5
  19. mapillary_tools/exiftool_read_video.py +12 -1
  20. mapillary_tools/exiftool_runner.py +77 -0
  21. mapillary_tools/geo.py +148 -107
  22. mapillary_tools/geotag/factory.py +298 -0
  23. mapillary_tools/geotag/geotag_from_generic.py +152 -11
  24. mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
  25. mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
  26. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
  27. mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
  28. mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
  29. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
  30. mapillary_tools/geotag/geotag_images_from_video.py +46 -46
  31. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
  32. mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
  33. mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
  34. mapillary_tools/geotag/options.py +159 -0
  35. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
  36. mapillary_tools/history.py +3 -11
  37. mapillary_tools/mp4/io_utils.py +0 -1
  38. mapillary_tools/mp4/mp4_sample_parser.py +11 -3
  39. mapillary_tools/mp4/simple_mp4_parser.py +0 -10
  40. mapillary_tools/process_geotag_properties.py +151 -386
  41. mapillary_tools/process_sequence_properties.py +554 -202
  42. mapillary_tools/sample_video.py +8 -15
  43. mapillary_tools/telemetry.py +24 -12
  44. mapillary_tools/types.py +80 -22
  45. mapillary_tools/upload.py +316 -298
  46. mapillary_tools/upload_api_v4.py +55 -122
  47. mapillary_tools/uploader.py +396 -254
  48. mapillary_tools/utils.py +26 -0
  49. mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
  50. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
  51. mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
  52. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
  53. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
  54. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
  55. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
  56. mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
  57. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
  58. mapillary_tools/geotag/utils.py +0 -26
  59. mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
  60. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  61. /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
  62. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
  63. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
  64. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.13.3a1"
1
+ VERSION = "0.14.0a1"
mapillary_tools/api_v4.py CHANGED
@@ -1,7 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
1
4
  import logging
2
5
  import os
3
6
  import ssl
4
7
  import typing as T
8
+ from json import dumps
5
9
 
6
10
  import requests
7
11
  from requests.adapters import HTTPAdapter
@@ -17,6 +21,12 @@ REQUESTS_TIMEOUT = 60 # 1 minutes
17
21
  USE_SYSTEM_CERTS: bool = False
18
22
 
19
23
 
24
+ class ClusterFileType(enum.Enum):
25
+ ZIP = "zip"
26
+ BLACKVUE = "mly_blackvue_video"
27
+ CAMM = "mly_camm_video"
28
+
29
+
20
30
  class HTTPSystemCertsAdapter(HTTPAdapter):
21
31
  """
22
32
  This adapter uses the system's certificate store instead of the certifi module.
@@ -46,22 +56,132 @@ class HTTPSystemCertsAdapter(HTTPAdapter):
46
56
  conn.ca_certs = None
47
57
 
48
58
 
59
+ @T.overload
60
+ def _truncate(s: bytes, limit: int = 512) -> bytes: ...
61
+
62
+
63
+ @T.overload
64
+ def _truncate(s: str, limit: int = 512) -> str: ...
65
+
66
+
67
+ def _truncate(s, limit=512):
68
+ if limit < len(s):
69
+ remaining = len(s) - limit
70
+ if isinstance(s, bytes):
71
+ return (
72
+ s[:limit]
73
+ + b"..."
74
+ + f"({remaining} more bytes truncated)".encode("utf-8")
75
+ )
76
+ else:
77
+ return str(s[:limit]) + f"...({remaining} more chars truncated)"
78
+ else:
79
+ return s
80
+
81
+
82
+ def _sanitize(headers: T.Mapping[T.Any, T.Any]) -> T.Mapping[T.Any, T.Any]:
83
+ new_headers = {}
84
+
85
+ for k, v in headers.items():
86
+ if k.lower() in [
87
+ "authorization",
88
+ "cookie",
89
+ "x-fb-access-token",
90
+ "access-token",
91
+ "access_token",
92
+ "password",
93
+ "user_upload_token",
94
+ ]:
95
+ new_headers[k] = "[REDACTED]"
96
+ else:
97
+ new_headers[k] = _truncate(v)
98
+
99
+ return new_headers
100
+
101
+
102
+ def _log_debug_request(
103
+ method: str,
104
+ url: str,
105
+ json: dict | None = None,
106
+ params: dict | None = None,
107
+ headers: dict | None = None,
108
+ timeout: T.Any = None,
109
+ ):
110
+ if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
111
+ return
112
+
113
+ msg = f"HTTP {method} {url}"
114
+
115
+ if USE_SYSTEM_CERTS:
116
+ msg += " (w/sys_certs)"
117
+
118
+ if json:
119
+ t = _truncate(dumps(_sanitize(json)))
120
+ msg += f" JSON={t}"
121
+
122
+ if params:
123
+ msg += f" PARAMS={_sanitize(params)}"
124
+
125
+ if headers:
126
+ msg += f" HEADERS={_sanitize(headers)}"
127
+
128
+ if timeout is not None:
129
+ msg += f" TIMEOUT={timeout}"
130
+
131
+ LOG.debug(msg)
132
+
133
+
134
+ def _log_debug_response(resp: requests.Response):
135
+ if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
136
+ return
137
+
138
+ data: T.Union[str, bytes]
139
+ try:
140
+ data = _truncate(dumps(_sanitize(resp.json())))
141
+ except Exception:
142
+ data = _truncate(resp.content)
143
+
144
+ LOG.debug(f"HTTP {resp.status_code} ({resp.reason}): %s", data)
145
+
146
+
147
+ def readable_http_error(ex: requests.HTTPError) -> str:
148
+ req = ex.request
149
+ resp = ex.response
150
+
151
+ data: T.Union[str, bytes]
152
+ try:
153
+ data = _truncate(dumps(_sanitize(resp.json())))
154
+ except Exception:
155
+ data = _truncate(resp.content)
156
+
157
+ return f"{req.method} {resp.url} => {resp.status_code} ({resp.reason}): {str(data)}"
158
+
159
+
49
160
  def request_post(
50
161
  url: str,
51
- data: T.Optional[T.Any] = None,
52
- json: T.Optional[dict] = None,
162
+ data: T.Any | None = None,
163
+ json: dict | None = None,
53
164
  **kwargs,
54
165
  ) -> requests.Response:
55
166
  global USE_SYSTEM_CERTS
56
167
 
168
+ _log_debug_request(
169
+ "POST",
170
+ url,
171
+ json=json,
172
+ params=kwargs.get("params"),
173
+ headers=kwargs.get("headers"),
174
+ timeout=kwargs.get("timeout"),
175
+ )
176
+
57
177
  if USE_SYSTEM_CERTS:
58
178
  with requests.Session() as session:
59
179
  session.mount("https://", HTTPSystemCertsAdapter())
60
- return session.post(url, data=data, json=json, **kwargs)
180
+ resp = session.post(url, data=data, json=json, **kwargs)
61
181
 
62
182
  else:
63
183
  try:
64
- return requests.post(url, data=data, json=json, **kwargs)
184
+ resp = requests.post(url, data=data, json=json, **kwargs)
65
185
  except requests.exceptions.SSLError as ex:
66
186
  if "SSLCertVerificationError" not in str(ex):
67
187
  raise ex
@@ -70,25 +190,35 @@ def request_post(
70
190
  LOG.warning(
71
191
  "SSL error occurred, falling back to system SSL certificates: %s", ex
72
192
  )
73
- with requests.Session() as session:
74
- session.mount("https://", HTTPSystemCertsAdapter())
75
- return session.post(url, data=data, json=json, **kwargs)
193
+ return request_post(url, data=data, json=json, **kwargs)
194
+
195
+ _log_debug_response(resp)
196
+
197
+ return resp
76
198
 
77
199
 
78
200
  def request_get(
79
201
  url: str,
80
- params: T.Optional[dict] = None,
202
+ params: dict | None = None,
81
203
  **kwargs,
82
204
  ) -> requests.Response:
83
205
  global USE_SYSTEM_CERTS
84
206
 
207
+ _log_debug_request(
208
+ "GET",
209
+ url,
210
+ params=kwargs.get("params"),
211
+ headers=kwargs.get("headers"),
212
+ timeout=kwargs.get("timeout"),
213
+ )
214
+
85
215
  if USE_SYSTEM_CERTS:
86
216
  with requests.Session() as session:
87
217
  session.mount("https://", HTTPSystemCertsAdapter())
88
- return session.get(url, params=params, **kwargs)
218
+ resp = session.get(url, params=params, **kwargs)
89
219
  else:
90
220
  try:
91
- return requests.get(url, params=params, **kwargs)
221
+ resp = requests.get(url, params=params, **kwargs)
92
222
  except requests.exceptions.SSLError as ex:
93
223
  if "SSLCertVerificationError" not in str(ex):
94
224
  raise ex
@@ -97,15 +227,55 @@ def request_get(
97
227
  LOG.warning(
98
228
  "SSL error occurred, falling back to system SSL certificates: %s", ex
99
229
  )
100
- with requests.Session() as session:
101
- session.mount("https://", HTTPSystemCertsAdapter())
102
- return session.get(url, params=params, **kwargs)
230
+ resp = request_get(url, params=params, **kwargs)
231
+
232
+ _log_debug_response(resp)
233
+
234
+ return resp
235
+
236
+
237
+ def is_auth_error(resp: requests.Response) -> bool:
238
+ if resp.status_code in [401, 403]:
239
+ return True
240
+
241
+ if resp.status_code in [400]:
242
+ try:
243
+ error_body = resp.json()
244
+ except Exception:
245
+ error_body = {}
246
+
247
+ type = error_body.get("debug_info", {}).get("type")
248
+ if type in ["NotAuthorizedError"]:
249
+ return True
250
+
251
+ return False
252
+
253
+
254
+ def extract_auth_error_message(resp: requests.Response) -> str:
255
+ assert is_auth_error(resp), "has to be an auth error"
256
+
257
+ try:
258
+ error_body = resp.json()
259
+ except Exception:
260
+ error_body = {}
261
+
262
+ # from Graph APIs
263
+ message = error_body.get("error", {}).get("message")
264
+ if message is not None:
265
+ return str(message)
266
+
267
+ # from upload service
268
+ message = error_body.get("debug_info", {}).get("message")
269
+ if message is not None:
270
+ return str(message)
271
+
272
+ return resp.text
103
273
 
104
274
 
105
275
  def get_upload_token(email: str, password: str) -> requests.Response:
106
276
  resp = request_post(
107
277
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
108
- params={"access_token": MAPILLARY_CLIENT_TOKEN},
278
+ headers={"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}"},
109
279
  json={"email": email, "password": password, "locale": "en_US"},
110
280
  timeout=REQUESTS_TIMEOUT,
111
281
  )
@@ -130,6 +300,30 @@ def fetch_organization(
130
300
  return resp
131
301
 
132
302
 
303
+ def fetch_user_or_me(
304
+ user_access_token: str,
305
+ user_id: int | str | None = None,
306
+ ) -> requests.Response:
307
+ if user_id is None:
308
+ url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
309
+ else:
310
+ url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
311
+
312
+ resp = request_get(
313
+ url,
314
+ params={
315
+ "fields": ",".join(["id", "username"]),
316
+ },
317
+ headers={
318
+ "Authorization": f"OAuth {user_access_token}",
319
+ },
320
+ timeout=REQUESTS_TIMEOUT,
321
+ )
322
+
323
+ resp.raise_for_status()
324
+ return resp
325
+
326
+
133
327
  ActionType = T.Literal[
134
328
  "upload_started_upload", "upload_finished_upload", "upload_failed_upload"
135
329
  ]
@@ -149,3 +343,30 @@ def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
149
343
  )
150
344
  resp.raise_for_status()
151
345
  return resp
346
+
347
+
348
+ def finish_upload(
349
+ user_access_token: str,
350
+ file_handle: str,
351
+ cluster_filetype: ClusterFileType,
352
+ organization_id: int | str | None = None,
353
+ ) -> requests.Response:
354
+ data: dict[str, str | int] = {
355
+ "file_handle": file_handle,
356
+ "file_type": cluster_filetype.value,
357
+ }
358
+ if organization_id is not None:
359
+ data["organization_id"] = organization_id
360
+
361
+ resp = request_post(
362
+ f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload",
363
+ headers={
364
+ "Authorization": f"OAuth {user_access_token}",
365
+ },
366
+ json=data,
367
+ timeout=REQUESTS_TIMEOUT,
368
+ )
369
+
370
+ resp.raise_for_status()
371
+
372
+ return resp