mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a2__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 (83) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +237 -16
  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 +429 -181
  7. mapillary_tools/commands/__main__.py +12 -6
  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 +31 -13
  15. mapillary_tools/constants.py +47 -6
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +221 -116
  18. mapillary_tools/exif_write.py +7 -7
  19. mapillary_tools/exiftool_read.py +33 -42
  20. mapillary_tools/exiftool_read_video.py +46 -33
  21. mapillary_tools/exiftool_runner.py +77 -0
  22. mapillary_tools/ffmpeg.py +24 -23
  23. mapillary_tools/geo.py +144 -120
  24. mapillary_tools/geotag/base.py +147 -0
  25. mapillary_tools/geotag/factory.py +291 -0
  26. mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
  27. mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
  28. mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
  29. mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
  30. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
  31. mapillary_tools/geotag/geotag_images_from_video.py +53 -51
  32. mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
  33. mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -0
  34. mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
  35. mapillary_tools/geotag/image_extractors/base.py +18 -0
  36. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  37. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  38. mapillary_tools/geotag/options.py +160 -0
  39. mapillary_tools/geotag/utils.py +52 -16
  40. mapillary_tools/geotag/video_extractors/base.py +18 -0
  41. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  42. mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
  43. mapillary_tools/geotag/video_extractors/native.py +157 -0
  44. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
  45. mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
  46. mapillary_tools/history.py +7 -13
  47. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  48. mapillary_tools/mp4/io_utils.py +0 -1
  49. mapillary_tools/mp4/mp4_sample_parser.py +36 -28
  50. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  51. mapillary_tools/mp4/simple_mp4_parser.py +13 -22
  52. mapillary_tools/process_geotag_properties.py +155 -392
  53. mapillary_tools/process_sequence_properties.py +562 -208
  54. mapillary_tools/sample_video.py +13 -20
  55. mapillary_tools/telemetry.py +26 -13
  56. mapillary_tools/types.py +111 -58
  57. mapillary_tools/upload.py +316 -298
  58. mapillary_tools/upload_api_v4.py +55 -122
  59. mapillary_tools/uploader.py +396 -254
  60. mapillary_tools/utils.py +42 -18
  61. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
  62. mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
  63. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
  64. mapillary_tools/geotag/__init__.py +0 -1
  65. mapillary_tools/geotag/geotag_from_generic.py +0 -22
  66. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
  67. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
  68. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  69. mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
  70. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  71. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
  72. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
  73. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
  74. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
  75. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  76. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
  77. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  78. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  79. mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
  80. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  81. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
  82. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
  83. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.13.3a1"
1
+ VERSION = "0.14.0a2"
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: 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: 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
  )
@@ -114,7 +284,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
114
284
 
115
285
 
116
286
  def fetch_organization(
117
- user_access_token: str, organization_id: T.Union[int, str]
287
+ user_access_token: str, organization_id: int | str
118
288
  ) -> requests.Response:
119
289
  resp = request_get(
120
290
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
@@ -130,12 +300,36 @@ 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
  ]
136
330
 
137
331
 
138
- def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
332
+ def log_event(action_type: ActionType, properties: dict) -> requests.Response:
139
333
  resp = request_post(
140
334
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
141
335
  json={
@@ -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