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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +66 -263
- mapillary_tools/authenticate.py +47 -39
- mapillary_tools/commands/__main__.py +15 -16
- mapillary_tools/commands/upload.py +33 -4
- mapillary_tools/config.py +5 -0
- 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/options.py +4 -1
- 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 +35 -38
- mapillary_tools/process_sequence_properties.py +339 -322
- mapillary_tools/sample_video.py +1 -2
- mapillary_tools/serializer/description.py +68 -58
- mapillary_tools/serializer/gpx.py +1 -1
- mapillary_tools/upload.py +202 -207
- mapillary_tools/upload_api_v4.py +57 -47
- mapillary_tools/uploader.py +728 -285
- mapillary_tools/utils.py +57 -5
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/METADATA +7 -6
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/RECORD +38 -37
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/WHEEL +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
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,
|
|
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("
|
|
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("
|
|
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.
|
|
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("
|
|
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("
|
|
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("
|
|
323
|
-
def
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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("
|
|
483
|
+
LOG.info(f"{len(typed_stats)} sequences uploaded")
|
|
387
484
|
else:
|
|
388
|
-
LOG.info("
|
|
485
|
+
LOG.info(f"{len(typed_stats)} {file_type} uploaded")
|
|
389
486
|
|
|
390
487
|
summary = _summarize(stats)
|
|
391
|
-
LOG.info("
|
|
392
|
-
LOG.info(
|
|
393
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
456
|
-
mly_uploader,
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
)
|