mapillary-tools 0.13.3__tar.gz → 0.14.0a1__tar.gz
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-0.13.3/mapillary_tools.egg-info → mapillary_tools-0.14.0a1}/PKG-INFO +3 -2
- mapillary_tools-0.14.0a1/mapillary_tools/__init__.py +1 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/api_v4.py +106 -7
- mapillary_tools-0.14.0a1/mapillary_tools/authenticate.py +363 -0
- {mapillary_tools-0.13.3/mapillary_tools/geotag → mapillary_tools-0.14.0a1/mapillary_tools}/blackvue_parser.py +74 -54
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools-0.14.0a1/mapillary_tools/camm/camm_parser.py +590 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/__main__.py +2 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/authenticate.py +8 -1
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/process.py +27 -51
- mapillary_tools-0.14.0a1/mapillary_tools/commands/process_and_upload.py +33 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/sample_video.py +2 -3
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/upload.py +18 -9
- mapillary_tools-0.14.0a1/mapillary_tools/commands/video_process_and_upload.py +33 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/config.py +28 -12
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/constants.py +46 -4
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/exceptions.py +34 -35
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/exif_read.py +158 -53
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/exiftool_read.py +19 -5
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools-0.14.0a1/mapillary_tools/exiftool_runner.py +77 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/geo.py +148 -107
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_from_generic.py +163 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_exif.py +60 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_exiftool.py +105 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +77 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_gpx.py +150 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_gpx_file.py +72 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_video.py +90 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +151 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_videos_from_video.py +165 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/options.py +159 -0
- {mapillary_tools-0.13.3/mapillary_tools/geotag → mapillary_tools-0.14.0a1/mapillary_tools/gpmf}/gpmf_parser.py +194 -171
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/history.py +3 -11
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/mp4/io_utils.py +0 -1
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools-0.14.0a1/mapillary_tools/process_geotag_properties.py +417 -0
- mapillary_tools-0.14.0a1/mapillary_tools/process_sequence_properties.py +697 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/sample_video.py +8 -15
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/telemetry.py +24 -12
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/types.py +80 -22
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/upload.py +311 -261
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/upload_api_v4.py +55 -95
- mapillary_tools-0.14.0a1/mapillary_tools/uploader.py +581 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/utils.py +26 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +49 -0
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/camm_parser.py +62 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +58 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1/mapillary_tools.egg-info}/PKG-INFO +3 -2
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools.egg-info/SOURCES.txt +8 -5
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/setup.py +1 -0
- mapillary_tools-0.13.3/mapillary_tools/__init__.py +0 -1
- mapillary_tools-0.13.3/mapillary_tools/authenticate.py +0 -102
- mapillary_tools-0.13.3/mapillary_tools/camm/camm_parser.py +0 -342
- mapillary_tools-0.13.3/mapillary_tools/commands/process_and_upload.py +0 -20
- mapillary_tools-0.13.3/mapillary_tools/commands/video_process_and_upload.py +0 -19
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_images_from_exif.py +0 -141
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -109
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -225
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -153
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_images_from_video.py +0 -90
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools-0.13.3/mapillary_tools/geotag/geotag_videos_from_video.py +0 -197
- mapillary_tools-0.13.3/mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3/mapillary_tools/process_geotag_properties.py +0 -652
- mapillary_tools-0.13.3/mapillary_tools/process_sequence_properties.py +0 -345
- mapillary_tools-0.13.3/mapillary_tools/uploader.py +0 -439
- mapillary_tools-0.13.3/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools-0.13.3/mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools-0.13.3/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/LICENSE +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/MANIFEST.in +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/README.md +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/__init__.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/video_process.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/commands/zip.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/exif_write.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/ffmpeg.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/geotag/__init__.py +0 -0
- {mapillary_tools-0.13.3/mapillary_tools/geotag → mapillary_tools-0.14.0a1/mapillary_tools/gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3/mapillary_tools/geotag → mapillary_tools-0.14.0a1/mapillary_tools/gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/ipc.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/mp4/__init__.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/mp4/construct_mp4_parser.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/cli_options.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools.egg-info/dependency_links.txt +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools.egg-info/entry_points.txt +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools.egg-info/requires.txt +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/mapillary_tools.egg-info/top_level.txt +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/requirements.txt +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/schema/image_description_schema.json +0 -0
- {mapillary_tools-0.13.3 → mapillary_tools-0.14.0a1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mapillary_tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0a1
|
|
4
4
|
Summary: Mapillary Image/Video Import Pipeline
|
|
5
5
|
Home-page: https://github.com/mapillary/mapillary_tools
|
|
6
6
|
Author: Mapillary
|
|
@@ -23,6 +23,7 @@ Dynamic: description
|
|
|
23
23
|
Dynamic: description-content-type
|
|
24
24
|
Dynamic: home-page
|
|
25
25
|
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
26
27
|
Dynamic: requires-dist
|
|
27
28
|
Dynamic: requires-python
|
|
28
29
|
Dynamic: summary
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.14.0a1"
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
1
4
|
import logging
|
|
2
5
|
import os
|
|
3
6
|
import ssl
|
|
@@ -18,6 +21,12 @@ REQUESTS_TIMEOUT = 60 # 1 minutes
|
|
|
18
21
|
USE_SYSTEM_CERTS: bool = False
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
class ClusterFileType(enum.Enum):
|
|
25
|
+
ZIP = "zip"
|
|
26
|
+
BLACKVUE = "mly_blackvue_video"
|
|
27
|
+
CAMM = "mly_camm_video"
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
class HTTPSystemCertsAdapter(HTTPAdapter):
|
|
22
31
|
"""
|
|
23
32
|
This adapter uses the system's certificate store instead of the certifi module.
|
|
@@ -70,7 +79,7 @@ def _truncate(s, limit=512):
|
|
|
70
79
|
return s
|
|
71
80
|
|
|
72
81
|
|
|
73
|
-
def _sanitize(headers: T.
|
|
82
|
+
def _sanitize(headers: T.Mapping[T.Any, T.Any]) -> T.Mapping[T.Any, T.Any]:
|
|
74
83
|
new_headers = {}
|
|
75
84
|
|
|
76
85
|
for k, v in headers.items():
|
|
@@ -81,6 +90,7 @@ def _sanitize(headers: T.Dict):
|
|
|
81
90
|
"access-token",
|
|
82
91
|
"access_token",
|
|
83
92
|
"password",
|
|
93
|
+
"user_upload_token",
|
|
84
94
|
]:
|
|
85
95
|
new_headers[k] = "[REDACTED]"
|
|
86
96
|
else:
|
|
@@ -92,9 +102,9 @@ def _sanitize(headers: T.Dict):
|
|
|
92
102
|
def _log_debug_request(
|
|
93
103
|
method: str,
|
|
94
104
|
url: str,
|
|
95
|
-
json:
|
|
96
|
-
params:
|
|
97
|
-
headers:
|
|
105
|
+
json: dict | None = None,
|
|
106
|
+
params: dict | None = None,
|
|
107
|
+
headers: dict | None = None,
|
|
98
108
|
timeout: T.Any = None,
|
|
99
109
|
):
|
|
100
110
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
@@ -149,8 +159,8 @@ def readable_http_error(ex: requests.HTTPError) -> str:
|
|
|
149
159
|
|
|
150
160
|
def request_post(
|
|
151
161
|
url: str,
|
|
152
|
-
data: T.
|
|
153
|
-
json:
|
|
162
|
+
data: T.Any | None = None,
|
|
163
|
+
json: dict | None = None,
|
|
154
164
|
**kwargs,
|
|
155
165
|
) -> requests.Response:
|
|
156
166
|
global USE_SYSTEM_CERTS
|
|
@@ -189,7 +199,7 @@ def request_post(
|
|
|
189
199
|
|
|
190
200
|
def request_get(
|
|
191
201
|
url: str,
|
|
192
|
-
params:
|
|
202
|
+
params: dict | None = None,
|
|
193
203
|
**kwargs,
|
|
194
204
|
) -> requests.Response:
|
|
195
205
|
global USE_SYSTEM_CERTS
|
|
@@ -224,6 +234,44 @@ def request_get(
|
|
|
224
234
|
return resp
|
|
225
235
|
|
|
226
236
|
|
|
237
|
+
def is_auth_error(resp: requests.Response) -> bool:
|
|
238
|
+
if resp.status_code in [401, 403]:
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
if resp.status_code in [400]:
|
|
242
|
+
try:
|
|
243
|
+
error_body = resp.json()
|
|
244
|
+
except Exception:
|
|
245
|
+
error_body = {}
|
|
246
|
+
|
|
247
|
+
type = error_body.get("debug_info", {}).get("type")
|
|
248
|
+
if type in ["NotAuthorizedError"]:
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def extract_auth_error_message(resp: requests.Response) -> str:
|
|
255
|
+
assert is_auth_error(resp), "has to be an auth error"
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
error_body = resp.json()
|
|
259
|
+
except Exception:
|
|
260
|
+
error_body = {}
|
|
261
|
+
|
|
262
|
+
# from Graph APIs
|
|
263
|
+
message = error_body.get("error", {}).get("message")
|
|
264
|
+
if message is not None:
|
|
265
|
+
return str(message)
|
|
266
|
+
|
|
267
|
+
# from upload service
|
|
268
|
+
message = error_body.get("debug_info", {}).get("message")
|
|
269
|
+
if message is not None:
|
|
270
|
+
return str(message)
|
|
271
|
+
|
|
272
|
+
return resp.text
|
|
273
|
+
|
|
274
|
+
|
|
227
275
|
def get_upload_token(email: str, password: str) -> requests.Response:
|
|
228
276
|
resp = request_post(
|
|
229
277
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
|
|
@@ -252,6 +300,30 @@ def fetch_organization(
|
|
|
252
300
|
return resp
|
|
253
301
|
|
|
254
302
|
|
|
303
|
+
def fetch_user_or_me(
|
|
304
|
+
user_access_token: str,
|
|
305
|
+
user_id: int | str | None = None,
|
|
306
|
+
) -> requests.Response:
|
|
307
|
+
if user_id is None:
|
|
308
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
|
|
309
|
+
else:
|
|
310
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
|
|
311
|
+
|
|
312
|
+
resp = request_get(
|
|
313
|
+
url,
|
|
314
|
+
params={
|
|
315
|
+
"fields": ",".join(["id", "username"]),
|
|
316
|
+
},
|
|
317
|
+
headers={
|
|
318
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
319
|
+
},
|
|
320
|
+
timeout=REQUESTS_TIMEOUT,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
resp.raise_for_status()
|
|
324
|
+
return resp
|
|
325
|
+
|
|
326
|
+
|
|
255
327
|
ActionType = T.Literal[
|
|
256
328
|
"upload_started_upload", "upload_finished_upload", "upload_failed_upload"
|
|
257
329
|
]
|
|
@@ -271,3 +343,30 @@ def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
|
|
|
271
343
|
)
|
|
272
344
|
resp.raise_for_status()
|
|
273
345
|
return resp
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def finish_upload(
|
|
349
|
+
user_access_token: str,
|
|
350
|
+
file_handle: str,
|
|
351
|
+
cluster_filetype: ClusterFileType,
|
|
352
|
+
organization_id: int | str | None = None,
|
|
353
|
+
) -> requests.Response:
|
|
354
|
+
data: dict[str, str | int] = {
|
|
355
|
+
"file_handle": file_handle,
|
|
356
|
+
"file_type": cluster_filetype.value,
|
|
357
|
+
}
|
|
358
|
+
if organization_id is not None:
|
|
359
|
+
data["organization_id"] = organization_id
|
|
360
|
+
|
|
361
|
+
resp = request_post(
|
|
362
|
+
f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload",
|
|
363
|
+
headers={
|
|
364
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
365
|
+
},
|
|
366
|
+
json=data,
|
|
367
|
+
timeout=REQUESTS_TIMEOUT,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
resp.raise_for_status()
|
|
371
|
+
|
|
372
|
+
return resp
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import typing as T
|
|
9
|
+
|
|
10
|
+
import jsonschema
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from . import api_v4, config, constants, exceptions, types
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOG = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def authenticate(
|
|
21
|
+
user_name: str | None = None,
|
|
22
|
+
user_email: str | None = None,
|
|
23
|
+
user_password: str | None = None,
|
|
24
|
+
jwt: str | None = None,
|
|
25
|
+
delete: bool = False,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Prompt for authentication information and save it to the config file
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# We still have to accept --user_name for the back compatibility
|
|
32
|
+
profile_name = user_name
|
|
33
|
+
|
|
34
|
+
all_user_items = config.list_all_users()
|
|
35
|
+
if all_user_items:
|
|
36
|
+
_list_all_profiles(all_user_items)
|
|
37
|
+
else:
|
|
38
|
+
_welcome()
|
|
39
|
+
|
|
40
|
+
# Make sure profile name either validated or existed
|
|
41
|
+
if profile_name is not None:
|
|
42
|
+
profile_name = profile_name.strip()
|
|
43
|
+
else:
|
|
44
|
+
if not _prompt_enabled():
|
|
45
|
+
raise exceptions.MapillaryBadParameterError(
|
|
46
|
+
"Profile name is required, please specify one with --user_name"
|
|
47
|
+
)
|
|
48
|
+
profile_name = _prompt_choose_profile_name(
|
|
49
|
+
list(all_user_items.keys()), must_exist=delete
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert profile_name is not None, "profile_name should be set"
|
|
53
|
+
|
|
54
|
+
if delete:
|
|
55
|
+
config.remove_config(profile_name)
|
|
56
|
+
LOG.info('Profile "%s" deleted', profile_name)
|
|
57
|
+
else:
|
|
58
|
+
if profile_name in all_user_items:
|
|
59
|
+
LOG.warning(
|
|
60
|
+
'The profile "%s" already exists and will be overridden',
|
|
61
|
+
profile_name,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
LOG.info('Creating new profile: "%s"', profile_name)
|
|
65
|
+
|
|
66
|
+
if jwt:
|
|
67
|
+
user_items: types.UserItem = {"user_upload_token": jwt}
|
|
68
|
+
user_items = _verify_user_auth(_validate_profile(user_items))
|
|
69
|
+
else:
|
|
70
|
+
user_items = _prompt_login(
|
|
71
|
+
user_email=user_email, user_password=user_password
|
|
72
|
+
)
|
|
73
|
+
_validate_profile(user_items)
|
|
74
|
+
|
|
75
|
+
# Update the config with the new user items
|
|
76
|
+
config.update_config(profile_name, user_items)
|
|
77
|
+
|
|
78
|
+
# TODO: print more user information
|
|
79
|
+
if profile_name in all_user_items:
|
|
80
|
+
LOG.info(
|
|
81
|
+
'Profile "%s" updated: %s', profile_name, api_v4._sanitize(user_items)
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
LOG.info(
|
|
85
|
+
'Profile "%s" created: %s', profile_name, api_v4._sanitize(user_items)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fetch_user_items(
|
|
90
|
+
user_name: str | None = None,
|
|
91
|
+
organization_key: str | None = None,
|
|
92
|
+
) -> types.UserItem:
|
|
93
|
+
"""
|
|
94
|
+
Read user information from the config file,
|
|
95
|
+
or prompt the user to authenticate if the specified profile does not exist
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# we still have to accept --user_name for the back compatibility
|
|
99
|
+
profile_name = user_name
|
|
100
|
+
|
|
101
|
+
all_user_items = config.list_all_users()
|
|
102
|
+
if not all_user_items:
|
|
103
|
+
authenticate(user_name=profile_name)
|
|
104
|
+
|
|
105
|
+
# Fetch user information only here
|
|
106
|
+
all_user_items = config.list_all_users()
|
|
107
|
+
assert len(all_user_items) >= 1, "should have at least 1 profile"
|
|
108
|
+
if profile_name is None:
|
|
109
|
+
if len(all_user_items) > 1:
|
|
110
|
+
if not _prompt_enabled():
|
|
111
|
+
raise exceptions.MapillaryBadParameterError(
|
|
112
|
+
"Multiple user profiles found, please choose one with --user_name"
|
|
113
|
+
)
|
|
114
|
+
_list_all_profiles(all_user_items)
|
|
115
|
+
profile_name = _prompt_choose_profile_name(
|
|
116
|
+
list(all_user_items.keys()), must_exist=True
|
|
117
|
+
)
|
|
118
|
+
user_items = all_user_items[profile_name]
|
|
119
|
+
else:
|
|
120
|
+
profile_name, user_items = list(all_user_items.items())[0]
|
|
121
|
+
else:
|
|
122
|
+
if profile_name in all_user_items:
|
|
123
|
+
user_items = all_user_items[profile_name]
|
|
124
|
+
else:
|
|
125
|
+
_list_all_profiles(all_user_items)
|
|
126
|
+
raise exceptions.MapillaryBadParameterError(
|
|
127
|
+
f'Profile "{profile_name}" not found'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert profile_name is not None, "profile_name should be set"
|
|
131
|
+
|
|
132
|
+
user_items = _verify_user_auth(_validate_profile(user_items))
|
|
133
|
+
|
|
134
|
+
LOG.info(
|
|
135
|
+
'Uploading to profile "%s": %s', profile_name, api_v4._sanitize(user_items)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if organization_key is not None:
|
|
139
|
+
resp = api_v4.fetch_organization(
|
|
140
|
+
user_items["user_upload_token"], organization_key
|
|
141
|
+
)
|
|
142
|
+
LOG.info("Uploading to Mapillary organization: %s", json.dumps(resp.json()))
|
|
143
|
+
user_items["MAPOrganizationKey"] = organization_key
|
|
144
|
+
|
|
145
|
+
return user_items
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _echo(*args, **kwargs):
|
|
149
|
+
print(*args, **kwargs, file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _prompt(message: str) -> str:
|
|
153
|
+
"""Display prompt on stderr and get input from stdin"""
|
|
154
|
+
print(message, end="", file=sys.stderr, flush=True)
|
|
155
|
+
return input()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _validate_profile(user_items: types.UserItem) -> types.UserItem:
|
|
159
|
+
try:
|
|
160
|
+
jsonschema.validate(user_items, types.UserItemSchema)
|
|
161
|
+
except jsonschema.ValidationError as ex:
|
|
162
|
+
raise exceptions.MapillaryBadParameterError(
|
|
163
|
+
f"Invalid profile format: {ex.message}"
|
|
164
|
+
)
|
|
165
|
+
return user_items
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _verify_user_auth(user_items: types.UserItem) -> types.UserItem:
|
|
169
|
+
"""
|
|
170
|
+
Verify that the user access token is valid
|
|
171
|
+
"""
|
|
172
|
+
if constants._AUTH_VERIFICATION_DISABLED:
|
|
173
|
+
return user_items
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
resp = api_v4.fetch_user_or_me(
|
|
177
|
+
user_access_token=user_items["user_upload_token"]
|
|
178
|
+
)
|
|
179
|
+
except requests.HTTPError as ex:
|
|
180
|
+
if api_v4.is_auth_error(ex.response):
|
|
181
|
+
message = api_v4.extract_auth_error_message(ex.response)
|
|
182
|
+
raise exceptions.MapillaryUploadUnauthorizedError(message)
|
|
183
|
+
else:
|
|
184
|
+
raise ex
|
|
185
|
+
|
|
186
|
+
user_json = resp.json()
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
**user_items,
|
|
190
|
+
"MAPSettingsUsername": user_json.get("username"),
|
|
191
|
+
"MAPSettingsUserKey": user_json.get("id"),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _validate_profile_name(profile_name: str):
|
|
196
|
+
if not (2 <= len(profile_name) <= 32):
|
|
197
|
+
raise exceptions.MapillaryBadParameterError(
|
|
198
|
+
"Profile name must be between 2 and 32 characters long"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
pattern = re.compile(r"^[a-zA-Z]+[a-zA-Z0-9_-]*$")
|
|
202
|
+
if not bool(pattern.match(profile_name)):
|
|
203
|
+
raise exceptions.MapillaryBadParameterError(
|
|
204
|
+
"Invalid profile name. Use only letters, numbers, hyphens and underscores"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _list_all_profiles(profiles: dict[str, types.UserItem]) -> None:
|
|
209
|
+
_echo("Existing Mapillary profiles:")
|
|
210
|
+
|
|
211
|
+
# Header
|
|
212
|
+
_echo(f"{'':>5} {'Profile name':<32} {'User ID':>16} {'Username':>32}")
|
|
213
|
+
|
|
214
|
+
# List all profiles
|
|
215
|
+
for idx, name in enumerate(profiles, 1):
|
|
216
|
+
items = profiles[name]
|
|
217
|
+
user_id = items.get("MAPSettingsUserKey", "N/A")
|
|
218
|
+
username = items.get("MAPSettingsUsername", "N/A")
|
|
219
|
+
_echo(f"{idx:>5}. {name:<32} {user_id:>16} {username:>32}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _is_interactive():
|
|
223
|
+
# Check if stdout is connected to a terminal
|
|
224
|
+
stdout_interactive = sys.stdout.isatty() if hasattr(sys.stdout, "isatty") else False
|
|
225
|
+
|
|
226
|
+
# Optionally, also check stdin and stderr
|
|
227
|
+
stdin_interactive = sys.stdin.isatty() if hasattr(sys.stdin, "isatty") else False
|
|
228
|
+
stderr_interactive = sys.stderr.isatty() if hasattr(sys.stderr, "isatty") else False
|
|
229
|
+
|
|
230
|
+
# Return True if any stream is interactive
|
|
231
|
+
return stdout_interactive or stdin_interactive or stderr_interactive
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _prompt_enabled() -> bool:
|
|
235
|
+
if constants.PROMPT_DISABLED:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
if not _is_interactive():
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _is_login_retryable(ex: requests.HTTPError) -> bool:
|
|
245
|
+
if 400 <= ex.response.status_code < 500:
|
|
246
|
+
r = ex.response.json()
|
|
247
|
+
subcode = r.get("error", {}).get("error_subcode")
|
|
248
|
+
if subcode in [1348028, 1348092, 3404005, 1348131]:
|
|
249
|
+
title = r.get("error", {}).get("error_user_title")
|
|
250
|
+
message = r.get("error", {}).get("error_user_msg")
|
|
251
|
+
LOG.error("%s: %s", title, message)
|
|
252
|
+
return True
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _prompt_login(
|
|
257
|
+
user_email: str | None = None,
|
|
258
|
+
user_password: str | None = None,
|
|
259
|
+
) -> types.UserItem:
|
|
260
|
+
_enabled = _prompt_enabled()
|
|
261
|
+
|
|
262
|
+
if user_email is None:
|
|
263
|
+
if not _enabled:
|
|
264
|
+
raise exceptions.MapillaryBadParameterError("user_email is required")
|
|
265
|
+
while not user_email:
|
|
266
|
+
user_email = _prompt("Enter Mapillary user email: ").strip()
|
|
267
|
+
else:
|
|
268
|
+
user_email = user_email.strip()
|
|
269
|
+
|
|
270
|
+
if user_password is None:
|
|
271
|
+
if not _enabled:
|
|
272
|
+
raise exceptions.MapillaryBadParameterError("user_password is required")
|
|
273
|
+
while True:
|
|
274
|
+
user_password = getpass.getpass("Enter Mapillary user password: ")
|
|
275
|
+
if user_password:
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
resp = api_v4.get_upload_token(user_email, user_password)
|
|
280
|
+
except requests.HTTPError as ex:
|
|
281
|
+
if not _enabled:
|
|
282
|
+
raise ex
|
|
283
|
+
|
|
284
|
+
if _is_login_retryable(ex):
|
|
285
|
+
return _prompt_login()
|
|
286
|
+
|
|
287
|
+
raise ex
|
|
288
|
+
|
|
289
|
+
data = resp.json()
|
|
290
|
+
|
|
291
|
+
user_items: types.UserItem = {
|
|
292
|
+
"user_upload_token": str(data["access_token"]),
|
|
293
|
+
"MAPSettingsUserKey": str(data["user_id"]),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return user_items
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _prompt_choose_profile_name(
|
|
300
|
+
existing_profile_names: T.Sequence[str], must_exist: bool = False
|
|
301
|
+
) -> str:
|
|
302
|
+
assert _prompt_enabled(), "should not get here if prompting is disabled"
|
|
303
|
+
|
|
304
|
+
existed = set(existing_profile_names)
|
|
305
|
+
|
|
306
|
+
while True:
|
|
307
|
+
if must_exist:
|
|
308
|
+
prompt = "Enter an existing profile: "
|
|
309
|
+
else:
|
|
310
|
+
prompt = "Enter an existing profile or create a new one: "
|
|
311
|
+
|
|
312
|
+
profile_name = _prompt(prompt).strip()
|
|
313
|
+
|
|
314
|
+
if not profile_name:
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# Exit if it's found
|
|
318
|
+
if profile_name in existed:
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
# Try to find by index
|
|
322
|
+
try:
|
|
323
|
+
profile_name = existing_profile_names[int(profile_name) - 1]
|
|
324
|
+
except (ValueError, IndexError):
|
|
325
|
+
pass
|
|
326
|
+
else:
|
|
327
|
+
# Exit if it's found
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
assert profile_name not in existed, (
|
|
331
|
+
f"Profile {profile_name} must not exist here"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if must_exist:
|
|
335
|
+
LOG.error('Profile "%s" not found', profile_name)
|
|
336
|
+
else:
|
|
337
|
+
try:
|
|
338
|
+
_validate_profile_name(profile_name)
|
|
339
|
+
except exceptions.MapillaryBadParameterError as ex:
|
|
340
|
+
LOG.error("Error validating profile name: %s", ex)
|
|
341
|
+
profile_name = ""
|
|
342
|
+
else:
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
if must_exist:
|
|
346
|
+
assert profile_name in existed, f"Profile {profile_name} must exist"
|
|
347
|
+
|
|
348
|
+
return profile_name
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _welcome():
|
|
352
|
+
_echo(
|
|
353
|
+
"""
|
|
354
|
+
================================================================================
|
|
355
|
+
Welcome to Mapillary!
|
|
356
|
+
================================================================================
|
|
357
|
+
If you haven't registered yet, please visit https://www.mapillary.com/signup
|
|
358
|
+
to create your account first.
|
|
359
|
+
|
|
360
|
+
Once registered, proceed here to sign in.
|
|
361
|
+
================================================================================
|
|
362
|
+
"""
|
|
363
|
+
)
|