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