mapillary-tools 0.14.0b1__py3-none-any.whl → 0.14.1__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 (36) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -263
  3. mapillary_tools/authenticate.py +46 -38
  4. mapillary_tools/commands/__main__.py +15 -16
  5. mapillary_tools/commands/upload.py +33 -4
  6. mapillary_tools/constants.py +127 -45
  7. mapillary_tools/exceptions.py +4 -0
  8. mapillary_tools/exif_read.py +2 -1
  9. mapillary_tools/exif_write.py +3 -1
  10. mapillary_tools/geo.py +16 -0
  11. mapillary_tools/geotag/base.py +6 -2
  12. mapillary_tools/geotag/factory.py +9 -1
  13. mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
  14. mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
  15. mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
  16. mapillary_tools/geotag/utils.py +9 -12
  17. mapillary_tools/geotag/video_extractors/gpx.py +2 -1
  18. mapillary_tools/geotag/video_extractors/native.py +25 -0
  19. mapillary_tools/history.py +124 -7
  20. mapillary_tools/http.py +211 -0
  21. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  22. mapillary_tools/process_geotag_properties.py +31 -27
  23. mapillary_tools/process_sequence_properties.py +339 -322
  24. mapillary_tools/sample_video.py +1 -2
  25. mapillary_tools/serializer/description.py +56 -56
  26. mapillary_tools/serializer/gpx.py +1 -1
  27. mapillary_tools/upload.py +201 -205
  28. mapillary_tools/upload_api_v4.py +57 -47
  29. mapillary_tools/uploader.py +720 -285
  30. mapillary_tools/utils.py +57 -5
  31. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +7 -6
  32. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/RECORD +36 -35
  33. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +0 -0
  34. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
  35. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
  36. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
mapillary_tools/upload.py CHANGED
@@ -9,6 +9,7 @@ import typing as T
9
9
  import uuid
10
10
  from pathlib import Path
11
11
 
12
+ import humanize
12
13
  import jsonschema
13
14
  import requests
14
15
  from tqdm import tqdm
@@ -18,18 +19,14 @@ from . import (
18
19
  config,
19
20
  constants,
20
21
  exceptions,
21
- geo,
22
22
  history,
23
+ http,
23
24
  ipc,
24
- telemetry,
25
25
  types,
26
26
  uploader,
27
27
  utils,
28
28
  VERSION,
29
29
  )
30
- from .camm import camm_builder, camm_parser
31
- from .gpmf import gpmf_parser
32
- from .mp4 import simple_mp4_builder
33
30
  from .serializer.description import DescriptionJSONSerializer
34
31
  from .types import FileType
35
32
 
@@ -38,18 +35,24 @@ JSONDict = T.Dict[str, T.Union[str, int, float, None]]
38
35
  LOG = logging.getLogger(__name__)
39
36
 
40
37
 
41
- class UploadedAlreadyError(uploader.SequenceError):
38
+ class UploadedAlready(uploader.SequenceError):
42
39
  pass
43
40
 
44
41
 
45
42
  def upload(
46
43
  import_path: Path | T.Sequence[Path],
47
44
  user_items: config.UserItem,
45
+ num_upload_workers: int,
48
46
  desc_path: str | None = None,
49
47
  _metadatas_from_process: T.Sequence[types.MetadataOrError] | None = None,
50
- dry_run=False,
51
- skip_subfolders=False,
48
+ reupload: bool = False,
49
+ dry_run: bool = False,
50
+ nofinish: bool = False,
51
+ noresume: bool = False,
52
+ skip_subfolders: bool = False,
52
53
  ) -> None:
54
+ LOG.info("==> Uploading...")
55
+
53
56
  import_paths = _normalize_import_paths(import_path)
54
57
 
55
58
  metadatas = _load_descs(_metadatas_from_process, import_paths, desc_path)
@@ -60,15 +63,8 @@ def upload(
60
63
 
61
64
  emitter = uploader.EventEmitter()
62
65
 
63
- # When dry_run mode is on, we disable history by default.
64
- # But we need dry_run for tests, so we added MAPILLARY__ENABLE_UPLOAD_HISTORY_FOR_DRY_RUN
65
- # and when it is on, we enable history regardless of dry_run
66
- enable_history = constants.MAPILLARY_UPLOAD_HISTORY_PATH and (
67
- not dry_run or constants.MAPILLARY__ENABLE_UPLOAD_HISTORY_FOR_DRY_RUN
68
- )
69
-
70
- # Put it first one to check duplications first
71
- if enable_history:
66
+ # Check duplications first
67
+ if not _is_history_disabled(dry_run):
72
68
  upload_run_params: JSONDict = {
73
69
  # Null if multiple paths provided
74
70
  "import_path": str(import_path) if isinstance(import_path, Path) else None,
@@ -77,7 +73,9 @@ def upload(
77
73
  "version": VERSION,
78
74
  "run_at": time.time(),
79
75
  }
80
- _setup_history(emitter, upload_run_params, metadatas)
76
+ _setup_history(
77
+ emitter, upload_run_params, metadatas, reupload=reupload, nofinish=nofinish
78
+ )
81
79
 
82
80
  # Set up tdqm
83
81
  _setup_tdqm(emitter)
@@ -88,7 +86,18 @@ def upload(
88
86
  # Send the progress via IPC, and log the progress in debug mode
89
87
  _setup_ipc(emitter)
90
88
 
91
- mly_uploader = uploader.Uploader(user_items, emitter=emitter, dry_run=dry_run)
89
+ try:
90
+ upload_options = uploader.UploadOptions(
91
+ user_items,
92
+ dry_run=dry_run,
93
+ nofinish=nofinish,
94
+ noresume=noresume,
95
+ num_upload_workers=num_upload_workers,
96
+ )
97
+ except ValueError as ex:
98
+ raise exceptions.MapillaryBadParameterError(str(ex)) from ex
99
+
100
+ mly_uploader = uploader.Uploader(upload_options, emitter=emitter)
92
101
 
93
102
  results = _gen_upload_everything(
94
103
  mly_uploader, metadatas, import_paths, skip_subfolders
@@ -97,23 +106,23 @@ def upload(
97
106
  upload_successes = 0
98
107
  upload_errors: list[Exception] = []
99
108
 
100
- # The real upload happens sequentially here
109
+ # The real uploading happens sequentially here
101
110
  try:
102
111
  for _, result in results:
103
112
  if result.error is not None:
104
- upload_errors.append(_continue_or_fail(result.error))
113
+ upload_error = _continue_or_fail(result.error)
114
+ log_exception(upload_error)
115
+ upload_errors.append(upload_error)
105
116
  else:
106
117
  upload_successes += 1
107
118
 
108
119
  except Exception as ex:
109
120
  # Fatal error: log and raise
110
- if not dry_run:
111
- _api_logging_failed(_summarize(stats), ex)
121
+ _api_logging_failed(_summarize(stats), ex, dry_run=dry_run)
112
122
  raise ex
113
123
 
114
124
  else:
115
- if not dry_run:
116
- _api_logging_finished(_summarize(stats))
125
+ _api_logging_finished(_summarize(stats), dry_run=dry_run)
117
126
 
118
127
  finally:
119
128
  # We collected stats after every upload is finished
@@ -139,38 +148,88 @@ def zip_images(import_path: Path, zip_dir: Path, desc_path: str | None = None):
139
148
  metadata for metadata in metadatas if isinstance(metadata, types.ImageMetadata)
140
149
  ]
141
150
 
142
- uploader.ZipImageSequence.zip_images(image_metadatas, zip_dir)
151
+ uploader.ZipUploader.zip_images(image_metadatas, zip_dir)
152
+
153
+
154
+ def log_exception(ex: Exception) -> None:
155
+ if LOG.isEnabledFor(logging.DEBUG):
156
+ exc_info = ex
157
+ else:
158
+ exc_info = None
159
+
160
+ exc_name = ex.__class__.__name__
161
+
162
+ if isinstance(ex, UploadedAlready):
163
+ LOG.info(f"{exc_name}: {ex}")
164
+ elif isinstance(ex, requests.HTTPError):
165
+ LOG.error(f"{exc_name}: {http.readable_http_error(ex)}", exc_info=exc_info)
166
+ elif isinstance(ex, api_v4.HTTPContentError):
167
+ LOG.error(
168
+ f"{exc_name}: {ex}: {http.readable_http_response(ex.response)}",
169
+ exc_info=exc_info,
170
+ )
171
+ else:
172
+ LOG.error(f"{exc_name}: {ex}", exc_info=exc_info)
173
+
174
+
175
+ def _is_history_disabled(dry_run: bool) -> bool:
176
+ # There is no way to read/write history if the path is not set
177
+ if not constants.MAPILLARY_UPLOAD_HISTORY_PATH:
178
+ return True
179
+
180
+ if dry_run:
181
+ # When dry_run mode is on, we disable history by default
182
+ # However, we need dry_run for tests, so we added MAPILLARY__ENABLE_UPLOAD_HISTORY_FOR_DRY_RUN
183
+ # and when it is on, we enable history regardless of dry_run
184
+ if constants.MAPILLARY__ENABLE_UPLOAD_HISTORY_FOR_DRY_RUN:
185
+ return False
186
+ else:
187
+ return True
188
+
189
+ return False
143
190
 
144
191
 
145
192
  def _setup_history(
146
193
  emitter: uploader.EventEmitter,
147
194
  upload_run_params: JSONDict,
148
195
  metadatas: list[types.Metadata],
196
+ reupload: bool,
197
+ nofinish: bool,
149
198
  ) -> None:
150
199
  @emitter.on("upload_start")
151
200
  def check_duplication(payload: uploader.Progress):
152
201
  md5sum = payload.get("sequence_md5sum")
153
202
  assert md5sum is not None, f"md5sum has to be set for {payload}"
154
203
 
155
- if history.is_uploaded(md5sum):
156
- sequence_uuid = payload.get("sequence_uuid")
157
- if sequence_uuid is None:
158
- basename = os.path.basename(payload.get("import_path", ""))
159
- LOG.info(
160
- "File %s has been uploaded already. Check the upload history at %s",
161
- basename,
162
- history.history_desc_path(md5sum),
163
- )
204
+ record = history.read_history_record(md5sum)
205
+
206
+ if record is not None:
207
+ history_desc_path = history.history_desc_path(md5sum)
208
+ uploaded_at = record.get("summary", {}).get("upload_end_time", None)
209
+
210
+ upload_name = uploader.Uploader._upload_name(payload)
211
+
212
+ if reupload:
213
+ if uploaded_at is not None:
214
+ LOG.info(
215
+ f"Reuploading {upload_name}, despite being uploaded {humanize.naturaldelta(time.time() - uploaded_at)} ago ({time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(uploaded_at))})"
216
+ )
217
+ else:
218
+ LOG.info(
219
+ f"Reuploading {upload_name}, despite already being uploaded (see {history_desc_path})"
220
+ )
164
221
  else:
165
- LOG.info(
166
- "Sequence %s has been uploaded already. Check the upload history at %s",
167
- sequence_uuid,
168
- history.history_desc_path(md5sum),
169
- )
170
- raise UploadedAlreadyError()
222
+ if uploaded_at is not None:
223
+ msg = f"Skipping {upload_name}, already uploaded {humanize.naturaldelta(time.time() - uploaded_at)} ago ({time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(uploaded_at))})"
224
+ else:
225
+ msg = f"Skipping {upload_name}, already uploaded (see {history_desc_path})"
226
+ raise UploadedAlready(msg)
171
227
 
172
228
  @emitter.on("upload_finished")
173
229
  def write_history(payload: uploader.Progress):
230
+ if nofinish:
231
+ return
232
+
174
233
  sequence_uuid = payload.get("sequence_uuid")
175
234
  md5sum = payload.get("sequence_md5sum")
176
235
  assert md5sum is not None, f"md5sum has to be set for {payload}"
@@ -188,10 +247,7 @@ def _setup_history(
188
247
 
189
248
  try:
190
249
  history.write_history(
191
- md5sum,
192
- upload_run_params,
193
- T.cast(JSONDict, payload),
194
- sequence,
250
+ md5sum, upload_run_params, T.cast(JSONDict, payload), sequence
195
251
  )
196
252
  except OSError:
197
253
  LOG.warning("Error writing upload history %s", md5sum, exc_info=True)
@@ -201,7 +257,6 @@ def _setup_tdqm(emitter: uploader.EventEmitter) -> None:
201
257
  upload_pbar: tqdm | None = None
202
258
 
203
259
  @emitter.on("upload_start")
204
- @emitter.on("upload_fetch_offset")
205
260
  def upload_start(payload: uploader.Progress) -> None:
206
261
  nonlocal upload_pbar
207
262
 
@@ -225,15 +280,40 @@ def _setup_tdqm(emitter: uploader.EventEmitter) -> None:
225
280
  unit_scale=True,
226
281
  unit_divisor=1024,
227
282
  initial=payload.get("offset", 0),
228
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
283
+ disable=LOG.isEnabledFor(logging.DEBUG),
229
284
  )
230
285
 
286
+ @emitter.on("upload_fetch_offset")
287
+ def upload_fetch_offset(payload: uploader.Progress) -> None:
288
+ assert upload_pbar is not None, (
289
+ "progress_bar must be initialized in upload_start"
290
+ )
291
+ begin_offset = payload.get("begin_offset", 0)
292
+ if begin_offset is not None and begin_offset > 0:
293
+ if upload_pbar.total is not None:
294
+ progress_percent = (begin_offset / upload_pbar.total) * 100
295
+ upload_pbar.write(
296
+ f"Resuming upload at {begin_offset=} ({progress_percent:3.0f}%)",
297
+ file=sys.stderr,
298
+ )
299
+ else:
300
+ upload_pbar.write(
301
+ f"Resuming upload at {begin_offset=}", file=sys.stderr
302
+ )
303
+ upload_pbar.reset()
304
+ upload_pbar.update(begin_offset)
305
+ upload_pbar.refresh()
306
+
231
307
  @emitter.on("upload_progress")
232
308
  def upload_progress(payload: uploader.Progress) -> None:
233
- assert upload_pbar is not None, "progress_bar must be initialized"
309
+ assert upload_pbar is not None, (
310
+ "progress_bar must be initialized in upload_start"
311
+ )
234
312
  upload_pbar.update(payload["chunk_size"])
313
+ upload_pbar.refresh()
235
314
 
236
315
  @emitter.on("upload_end")
316
+ @emitter.on("upload_failed")
237
317
  def upload_end(_: uploader.Progress) -> None:
238
318
  nonlocal upload_pbar
239
319
  if upload_pbar:
@@ -245,20 +325,20 @@ def _setup_ipc(emitter: uploader.EventEmitter):
245
325
  @emitter.on("upload_start")
246
326
  def upload_start(payload: uploader.Progress):
247
327
  type: uploader.EventName = "upload_start"
248
- LOG.debug("IPC %s: %s", type.upper(), payload)
328
+ LOG.debug(f"{type.upper()}: {json.dumps(payload)}")
249
329
  ipc.send(type, payload)
250
330
 
251
331
  @emitter.on("upload_fetch_offset")
252
332
  def upload_fetch_offset(payload: uploader.Progress) -> None:
253
333
  type: uploader.EventName = "upload_fetch_offset"
254
- LOG.debug("IPC %s: %s", type.upper(), payload)
334
+ LOG.debug(f"{type.upper()}: {json.dumps(payload)}")
255
335
  ipc.send(type, payload)
256
336
 
257
337
  @emitter.on("upload_progress")
258
338
  def upload_progress(payload: uploader.Progress):
259
339
  type: uploader.EventName = "upload_progress"
260
340
 
261
- if LOG.getEffectiveLevel() <= logging.DEBUG:
341
+ if LOG.isEnabledFor(logging.DEBUG):
262
342
  # In debug mode, we want to see the progress every 30 seconds
263
343
  # instead of every chunk (which is too verbose)
264
344
  INTERVAL_SECONDS = 30
@@ -270,7 +350,7 @@ def _setup_ipc(emitter: uploader.EventEmitter):
270
350
  last_upload_progress_debug_at is None
271
351
  or last_upload_progress_debug_at + INTERVAL_SECONDS < now
272
352
  ):
273
- LOG.debug("IPC %s: %s", type.upper(), payload)
353
+ LOG.debug(f"{type.upper()}: {json.dumps(payload)}")
274
354
  T.cast(T.Dict, payload)["_last_upload_progress_debug_at"] = now
275
355
 
276
356
  ipc.send(type, payload)
@@ -278,7 +358,13 @@ def _setup_ipc(emitter: uploader.EventEmitter):
278
358
  @emitter.on("upload_end")
279
359
  def upload_end(payload: uploader.Progress) -> None:
280
360
  type: uploader.EventName = "upload_end"
281
- LOG.debug("IPC %s: %s", type.upper(), payload)
361
+ LOG.debug(f"{type.upper()}: {json.dumps(payload)}")
362
+ ipc.send(type, payload)
363
+
364
+ @emitter.on("upload_failed")
365
+ def upload_failed(payload: uploader.Progress) -> None:
366
+ type: uploader.EventName = "upload_failed"
367
+ LOG.debug(f"{type.upper()}: {json.dumps(payload)}")
282
368
  ipc.send(type, payload)
283
369
 
284
370
 
@@ -319,8 +405,8 @@ def _setup_api_stats(emitter: uploader.EventEmitter):
319
405
  payload["offset"], payload.get("upload_first_offset", payload["offset"])
320
406
  )
321
407
 
322
- @emitter.on("upload_interrupted")
323
- def collect_interrupted(payload: _APIStats):
408
+ @emitter.on("upload_retrying")
409
+ def collect_retrying(payload: _APIStats):
324
410
  # could be None if it failed to fetch offset
325
411
  restart_time = payload.get("upload_last_restart_time")
326
412
  if restart_time is not None:
@@ -374,61 +460,80 @@ def _summarize(stats: T.Sequence[_APIStats]) -> dict:
374
460
 
375
461
 
376
462
  def _show_upload_summary(stats: T.Sequence[_APIStats], errors: T.Sequence[Exception]):
377
- if not stats:
378
- LOG.info("Nothing uploaded. Bye.")
379
- else:
463
+ LOG.info("==> Upload summary")
464
+
465
+ errors_by_type: dict[type[Exception], list[Exception]] = {}
466
+ for error in errors:
467
+ errors_by_type.setdefault(type(error), []).append(error)
468
+
469
+ for error_type, error_list in errors_by_type.items():
470
+ if error_type is UploadedAlready:
471
+ LOG.info(
472
+ f"Skipped {len(error_list)} already uploaded sequences (use --reupload to force re-upload)",
473
+ )
474
+ else:
475
+ LOG.info(f"{len(error_list)} uploads failed due to {error_type.__name__}")
476
+
477
+ if stats:
380
478
  grouped: dict[str, list[_APIStats]] = {}
381
479
  for stat in stats:
382
480
  grouped.setdefault(stat.get("file_type", "unknown"), []).append(stat)
383
481
 
384
482
  for file_type, typed_stats in grouped.items():
385
483
  if file_type == FileType.IMAGE.value:
386
- LOG.info("%8d image sequences uploaded", len(typed_stats))
484
+ LOG.info(f"{len(typed_stats)} sequences uploaded")
387
485
  else:
388
- LOG.info("%8d %s videos uploaded", len(typed_stats), file_type.upper())
486
+ LOG.info(f"{len(typed_stats)} {file_type} uploaded")
389
487
 
390
488
  summary = _summarize(stats)
391
- LOG.info("%8.1fM data in total", summary["size"])
392
- LOG.info("%8.1fM data uploaded", summary["uploaded_size"])
393
- LOG.info("%8.1fs upload time", summary["time"])
489
+ LOG.info(f"{humanize.naturalsize(summary['size'] * 1024 * 1024)} read in total")
490
+ LOG.info(
491
+ f"{humanize.naturalsize(summary['uploaded_size'] * 1024 * 1024)} uploaded"
492
+ )
493
+ LOG.info(f"{summary['time']:.3f} seconds upload time")
494
+ else:
495
+ LOG.info("Nothing uploaded. Bye.")
394
496
 
395
- for error in errors:
396
- LOG.error("Upload error: %s: %s", error.__class__.__name__, error)
397
497
 
498
+ def _api_logging_finished(summary: dict, dry_run: bool = False):
499
+ if dry_run:
500
+ return
398
501
 
399
- def _api_logging_finished(summary: dict):
400
502
  if constants.MAPILLARY_DISABLE_API_LOGGING:
401
503
  return
402
504
 
403
505
  action: api_v4.ActionType = "upload_finished_upload"
404
- try:
405
- api_v4.log_event(action, summary)
406
- except requests.HTTPError as exc:
407
- LOG.warning(
408
- "HTTPError from API Logging for action %s: %s",
409
- action,
410
- api_v4.readable_http_error(exc),
411
- )
412
- except Exception:
413
- LOG.warning("Error from API Logging for action %s", action, exc_info=True)
414
506
 
507
+ with api_v4.create_client_session(disable_logging=True) as client_session:
508
+ try:
509
+ api_v4.log_event(client_session, action, summary)
510
+ except requests.HTTPError as exc:
511
+ LOG.warning(
512
+ f"HTTPError from logging action {action}: {http.readable_http_error(exc)}"
513
+ )
514
+ except Exception:
515
+ LOG.warning(f"Error from logging action {action}", exc_info=True)
516
+
517
+
518
+ def _api_logging_failed(payload: dict, exc: Exception, dry_run: bool = False):
519
+ if dry_run:
520
+ return
415
521
 
416
- def _api_logging_failed(payload: dict, exc: Exception):
417
522
  if constants.MAPILLARY_DISABLE_API_LOGGING:
418
523
  return
419
524
 
420
525
  payload_with_reason = {**payload, "reason": exc.__class__.__name__}
421
526
  action: api_v4.ActionType = "upload_failed_upload"
422
- try:
423
- api_v4.log_event(action, payload_with_reason)
424
- except requests.HTTPError as exc:
425
- LOG.warning(
426
- "HTTPError from API Logging for action %s: %s",
427
- action,
428
- api_v4.readable_http_error(exc),
429
- )
430
- except Exception:
431
- LOG.warning("Error from API Logging for action %s", action, exc_info=True)
527
+
528
+ with api_v4.create_client_session(disable_logging=True) as client_session:
529
+ try:
530
+ api_v4.log_event(client_session, action, payload_with_reason)
531
+ except requests.HTTPError as exc:
532
+ LOG.warning(
533
+ f"HTTPError from logging action {action}: {http.readable_http_error(exc)}"
534
+ )
535
+ except Exception:
536
+ LOG.warning(f"Error from logging action {action}", exc_info=True)
432
537
 
433
538
 
434
539
  _M = T.TypeVar("_M", bound=types.Metadata)
@@ -452,109 +557,21 @@ def _gen_upload_everything(
452
557
  (m for m in metadatas if isinstance(m, types.ImageMetadata)),
453
558
  utils.find_images(import_paths, skip_subfolders=skip_subfolders),
454
559
  )
455
- for image_result in uploader.ZipImageSequence.upload_images(
456
- mly_uploader,
457
- image_metadatas,
458
- ):
459
- yield image_result
560
+ image_uploader = uploader.ImageSequenceUploader(
561
+ mly_uploader.upload_options, emitter=mly_uploader.emitter
562
+ )
563
+ yield from image_uploader.upload_images(image_metadatas)
460
564
 
461
565
  # Upload videos
462
566
  video_metadatas = _find_metadata_with_filename_existed_in(
463
567
  (m for m in metadatas if isinstance(m, types.VideoMetadata)),
464
568
  utils.find_videos(import_paths, skip_subfolders=skip_subfolders),
465
569
  )
466
- for video_result in _gen_upload_videos(mly_uploader, video_metadatas):
467
- yield video_result
570
+ yield from uploader.VideoUploader.upload_videos(mly_uploader, video_metadatas)
468
571
 
469
572
  # Upload zip files
470
573
  zip_paths = utils.find_zipfiles(import_paths, skip_subfolders=skip_subfolders)
471
- for zip_result in _gen_upload_zipfiles(mly_uploader, zip_paths):
472
- yield zip_result
473
-
474
-
475
- def _gen_upload_videos(
476
- mly_uploader: uploader.Uploader, video_metadatas: T.Sequence[types.VideoMetadata]
477
- ) -> T.Generator[tuple[types.VideoMetadata, uploader.UploadResult], None, None]:
478
- for idx, video_metadata in enumerate(video_metadatas):
479
- try:
480
- video_metadata.update_md5sum()
481
- except Exception as ex:
482
- yield video_metadata, uploader.UploadResult(error=ex)
483
- continue
484
-
485
- assert isinstance(video_metadata.md5sum, str), "md5sum should be updated"
486
-
487
- # Convert video metadata to CAMMInfo
488
- camm_info = _prepare_camm_info(video_metadata)
489
-
490
- # Create the CAMM sample generator
491
- camm_sample_generator = camm_builder.camm_sample_generator2(camm_info)
492
-
493
- progress: uploader.SequenceProgress = {
494
- "total_sequence_count": len(video_metadatas),
495
- "sequence_idx": idx,
496
- "file_type": video_metadata.filetype.value,
497
- "import_path": str(video_metadata.filename),
498
- "sequence_md5sum": video_metadata.md5sum,
499
- }
500
-
501
- try:
502
- with video_metadata.filename.open("rb") as src_fp:
503
- # Build the mp4 stream with the CAMM samples
504
- camm_fp = simple_mp4_builder.transform_mp4(
505
- src_fp, camm_sample_generator
506
- )
507
-
508
- # Upload the mp4 stream
509
- file_handle = mly_uploader.upload_stream(
510
- T.cast(T.IO[bytes], camm_fp),
511
- progress=T.cast(T.Dict[str, T.Any], progress),
512
- )
513
- cluster_id = mly_uploader.finish_upload(
514
- file_handle,
515
- api_v4.ClusterFileType.CAMM,
516
- progress=T.cast(T.Dict[str, T.Any], progress),
517
- )
518
- except Exception as ex:
519
- yield video_metadata, uploader.UploadResult(error=ex)
520
- else:
521
- yield video_metadata, uploader.UploadResult(result=cluster_id)
522
-
523
-
524
- def _prepare_camm_info(video_metadata: types.VideoMetadata) -> camm_parser.CAMMInfo:
525
- camm_info = camm_parser.CAMMInfo(
526
- make=video_metadata.make or "", model=video_metadata.model or ""
527
- )
528
-
529
- for point in video_metadata.points:
530
- if isinstance(point, telemetry.CAMMGPSPoint):
531
- if camm_info.gps is None:
532
- camm_info.gps = []
533
- camm_info.gps.append(point)
534
-
535
- elif isinstance(point, telemetry.GPSPoint):
536
- # There is no proper CAMM entry for GoPro GPS
537
- if camm_info.mini_gps is None:
538
- camm_info.mini_gps = []
539
- camm_info.mini_gps.append(point)
540
-
541
- elif isinstance(point, geo.Point):
542
- if camm_info.mini_gps is None:
543
- camm_info.mini_gps = []
544
- camm_info.mini_gps.append(point)
545
- else:
546
- raise ValueError(f"Unknown point type: {point}")
547
-
548
- if constants.MAPILLARY__EXPERIMENTAL_ENABLE_IMU:
549
- if video_metadata.filetype is FileType.GOPRO:
550
- with video_metadata.filename.open("rb") as fp:
551
- gopro_info = gpmf_parser.extract_gopro_info(fp, telemetry_only=True)
552
- if gopro_info is not None:
553
- camm_info.accl = gopro_info.accl or []
554
- camm_info.gyro = gopro_info.gyro or []
555
- camm_info.magn = gopro_info.magn or []
556
-
557
- return camm_info
574
+ yield from uploader.ZipUploader.upload_zipfiles(mly_uploader, zip_paths)
558
575
 
559
576
 
560
577
  def _normalize_import_paths(import_path: Path | T.Sequence[Path]) -> list[Path]:
@@ -587,7 +604,7 @@ def _continue_or_fail(ex: Exception) -> Exception:
587
604
  return ex
588
605
 
589
606
  # Certain files not found or no permission
590
- if isinstance(ex, OSError):
607
+ if isinstance(ex, (FileNotFoundError, PermissionError)):
591
608
  return ex
592
609
 
593
610
  # Certain metadatas are not valid
@@ -615,27 +632,6 @@ def _continue_or_fail(ex: Exception) -> Exception:
615
632
  raise ex
616
633
 
617
634
 
618
- def _gen_upload_zipfiles(
619
- mly_uploader: uploader.Uploader, zip_paths: T.Sequence[Path]
620
- ) -> T.Generator[tuple[Path, uploader.UploadResult], None, None]:
621
- for idx, zip_path in enumerate(zip_paths):
622
- progress: uploader.SequenceProgress = {
623
- "total_sequence_count": len(zip_paths),
624
- "sequence_idx": idx,
625
- "import_path": str(zip_path),
626
- "file_type": types.FileType.ZIP.value,
627
- "sequence_md5sum": "", # Placeholder, will be set in upload_zipfile
628
- }
629
- try:
630
- cluster_id = uploader.ZipImageSequence.upload_zipfile(
631
- mly_uploader, zip_path, progress=T.cast(T.Dict[str, T.Any], progress)
632
- )
633
- except Exception as ex:
634
- yield zip_path, uploader.UploadResult(error=ex)
635
- else:
636
- yield zip_path, uploader.UploadResult(result=cluster_id)
637
-
638
-
639
635
  def _load_descs(
640
636
  _metadatas_from_process: T.Sequence[types.MetadataOrError] | None,
641
637
  import_paths: T.Sequence[Path],
@@ -700,9 +696,9 @@ def _find_desc_path(import_paths: T.Sequence[Path]) -> str:
700
696
 
701
697
  if 1 < len(import_paths):
702
698
  raise exceptions.MapillaryBadParameterError(
703
- "The description path must be specified (with --desc_path) when uploading multiple paths",
699
+ "The description path must be specified (with --desc_path) when uploading multiple paths"
704
700
  )
705
701
  else:
706
702
  raise exceptions.MapillaryBadParameterError(
707
- "The description path must be specified (with --desc_path) when uploading a single file",
703
+ "The description path must be specified (with --desc_path) when uploading a single file"
708
704
  )