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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +66 -263
- mapillary_tools/authenticate.py +46 -38
- mapillary_tools/commands/__main__.py +15 -16
- mapillary_tools/commands/upload.py +33 -4
- mapillary_tools/constants.py +127 -45
- mapillary_tools/exceptions.py +4 -0
- mapillary_tools/exif_read.py +2 -1
- mapillary_tools/exif_write.py +3 -1
- mapillary_tools/geo.py +16 -0
- mapillary_tools/geotag/base.py +6 -2
- mapillary_tools/geotag/factory.py +9 -1
- mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
- mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
- mapillary_tools/geotag/utils.py +9 -12
- mapillary_tools/geotag/video_extractors/gpx.py +2 -1
- mapillary_tools/geotag/video_extractors/native.py +25 -0
- mapillary_tools/history.py +124 -7
- mapillary_tools/http.py +211 -0
- mapillary_tools/mp4/construct_mp4_parser.py +8 -2
- mapillary_tools/process_geotag_properties.py +31 -27
- mapillary_tools/process_sequence_properties.py +339 -322
- mapillary_tools/sample_video.py +1 -2
- mapillary_tools/serializer/description.py +56 -56
- mapillary_tools/serializer/gpx.py +1 -1
- mapillary_tools/upload.py +201 -205
- mapillary_tools/upload_api_v4.py +57 -47
- mapillary_tools/uploader.py +720 -285
- mapillary_tools/utils.py +57 -5
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +7 -6
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/RECORD +36 -35
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
#
|
|
64
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
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,
|
|
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("
|
|
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("
|
|
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.
|
|
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("
|
|
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("
|
|
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("
|
|
323
|
-
def
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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("
|
|
484
|
+
LOG.info(f"{len(typed_stats)} sequences uploaded")
|
|
387
485
|
else:
|
|
388
|
-
LOG.info("
|
|
486
|
+
LOG.info(f"{len(typed_stats)} {file_type} uploaded")
|
|
389
487
|
|
|
390
488
|
summary = _summarize(stats)
|
|
391
|
-
LOG.info("
|
|
392
|
-
LOG.info(
|
|
393
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
456
|
-
mly_uploader,
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
)
|