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/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.14.
|
|
1
|
+
VERSION = "0.14.1"
|
mapillary_tools/api_v4.py
CHANGED
|
@@ -3,12 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import enum
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
-
import ssl
|
|
7
6
|
import typing as T
|
|
8
|
-
from json import dumps
|
|
9
7
|
|
|
10
8
|
import requests
|
|
11
|
-
|
|
9
|
+
|
|
10
|
+
from . import http
|
|
12
11
|
|
|
13
12
|
LOG = logging.getLogger(__name__)
|
|
14
13
|
MAPILLARY_CLIENT_TOKEN = os.getenv(
|
|
@@ -17,222 +16,40 @@ MAPILLARY_CLIENT_TOKEN = os.getenv(
|
|
|
17
16
|
MAPILLARY_GRAPH_API_ENDPOINT = os.getenv(
|
|
18
17
|
"MAPILLARY_GRAPH_API_ENDPOINT", "https://graph.mapillary.com"
|
|
19
18
|
)
|
|
20
|
-
REQUESTS_TIMEOUT = 60 # 1 minutes
|
|
21
|
-
USE_SYSTEM_CERTS: bool = False
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ClusterFileType(enum.Enum):
|
|
25
|
-
ZIP = "zip"
|
|
26
|
-
BLACKVUE = "mly_blackvue_video"
|
|
27
|
-
CAMM = "mly_camm_video"
|
|
28
|
-
MLY_BUNDLE_MANIFEST = "mly_bundle_manifest"
|
|
19
|
+
REQUESTS_TIMEOUT: float = 60 # 1 minutes
|
|
29
20
|
|
|
30
21
|
|
|
31
|
-
class
|
|
22
|
+
class HTTPContentError(Exception):
|
|
32
23
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
The implementation is based on the project https://pypi.org/project/pip-system-certs/,
|
|
36
|
-
which has a system-wide effect.
|
|
24
|
+
Raised when the HTTP response is ok (200) but the content is not as expected
|
|
25
|
+
e.g. not JSON or not a valid response.
|
|
37
26
|
"""
|
|
38
27
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
kwargs["ssl_context"] = ssl_context
|
|
43
|
-
|
|
44
|
-
super().init_poolmanager(*args, **kwargs)
|
|
45
|
-
|
|
46
|
-
def cert_verify(self, *args, **kwargs):
|
|
47
|
-
super().cert_verify(*args, **kwargs)
|
|
48
|
-
|
|
49
|
-
# By default Python requests uses the ca_certs from the certifi module
|
|
50
|
-
# But we want to use the certificate store instead.
|
|
51
|
-
# By clearing the ca_certs variable we force it to fall back on that behaviour (handled in urllib3)
|
|
52
|
-
if "conn" in kwargs:
|
|
53
|
-
conn = kwargs["conn"]
|
|
54
|
-
else:
|
|
55
|
-
conn = args[0]
|
|
56
|
-
|
|
57
|
-
conn.ca_certs = None
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@T.overload
|
|
61
|
-
def _truncate(s: bytes, limit: int = 512) -> bytes: ...
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@T.overload
|
|
65
|
-
def _truncate(s: str, limit: int = 512) -> str: ...
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _truncate(s, limit=512):
|
|
69
|
-
if limit < len(s):
|
|
70
|
-
remaining = len(s) - limit
|
|
71
|
-
if isinstance(s, bytes):
|
|
72
|
-
return (
|
|
73
|
-
s[:limit]
|
|
74
|
-
+ b"..."
|
|
75
|
-
+ f"({remaining} more bytes truncated)".encode("utf-8")
|
|
76
|
-
)
|
|
77
|
-
else:
|
|
78
|
-
return str(s[:limit]) + f"...({remaining} more chars truncated)"
|
|
79
|
-
else:
|
|
80
|
-
return s
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _sanitize(headers: T.Mapping[T.Any, T.Any]) -> T.Mapping[T.Any, T.Any]:
|
|
84
|
-
new_headers = {}
|
|
85
|
-
|
|
86
|
-
for k, v in headers.items():
|
|
87
|
-
if k.lower() in [
|
|
88
|
-
"authorization",
|
|
89
|
-
"cookie",
|
|
90
|
-
"x-fb-access-token",
|
|
91
|
-
"access-token",
|
|
92
|
-
"access_token",
|
|
93
|
-
"password",
|
|
94
|
-
"user_upload_token",
|
|
95
|
-
]:
|
|
96
|
-
new_headers[k] = "[REDACTED]"
|
|
97
|
-
else:
|
|
98
|
-
new_headers[k] = _truncate(v)
|
|
99
|
-
|
|
100
|
-
return new_headers
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _log_debug_request(
|
|
104
|
-
method: str,
|
|
105
|
-
url: str,
|
|
106
|
-
json: dict | None = None,
|
|
107
|
-
params: dict | None = None,
|
|
108
|
-
headers: dict | None = None,
|
|
109
|
-
timeout: T.Any = None,
|
|
110
|
-
):
|
|
111
|
-
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
msg = f"HTTP {method} {url}"
|
|
115
|
-
|
|
116
|
-
if USE_SYSTEM_CERTS:
|
|
117
|
-
msg += " (w/sys_certs)"
|
|
118
|
-
|
|
119
|
-
if json:
|
|
120
|
-
t = _truncate(dumps(_sanitize(json)))
|
|
121
|
-
msg += f" JSON={t}"
|
|
122
|
-
|
|
123
|
-
if params:
|
|
124
|
-
msg += f" PARAMS={_sanitize(params)}"
|
|
125
|
-
|
|
126
|
-
if headers:
|
|
127
|
-
msg += f" HEADERS={_sanitize(headers)}"
|
|
128
|
-
|
|
129
|
-
if timeout is not None:
|
|
130
|
-
msg += f" TIMEOUT={timeout}"
|
|
131
|
-
|
|
132
|
-
LOG.debug(msg)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _log_debug_response(resp: requests.Response):
|
|
136
|
-
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
data: str | bytes
|
|
140
|
-
try:
|
|
141
|
-
data = _truncate(dumps(_sanitize(resp.json())))
|
|
142
|
-
except Exception:
|
|
143
|
-
data = _truncate(resp.content)
|
|
28
|
+
def __init__(self, message: str, response: requests.Response):
|
|
29
|
+
self.response = response
|
|
30
|
+
super().__init__(message)
|
|
144
31
|
|
|
145
|
-
LOG.debug(f"HTTP {resp.status_code} ({resp.reason}): %s", data)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def readable_http_error(ex: requests.HTTPError) -> str:
|
|
149
|
-
req = ex.request
|
|
150
|
-
resp = ex.response
|
|
151
|
-
|
|
152
|
-
data: str | bytes
|
|
153
|
-
try:
|
|
154
|
-
data = _truncate(dumps(_sanitize(resp.json())))
|
|
155
|
-
except Exception:
|
|
156
|
-
data = _truncate(resp.content)
|
|
157
|
-
|
|
158
|
-
return f"{req.method} {resp.url} => {resp.status_code} ({resp.reason}): {str(data)}"
|
|
159
32
|
|
|
33
|
+
class ClusterFileType(enum.Enum):
|
|
34
|
+
ZIP = "zip"
|
|
35
|
+
BLACKVUE = "mly_blackvue_video"
|
|
36
|
+
CAMM = "mly_camm_video"
|
|
37
|
+
MLY_BUNDLE_MANIFEST = "mly_bundle_manifest"
|
|
160
38
|
|
|
161
|
-
def request_post(
|
|
162
|
-
url: str,
|
|
163
|
-
data: T.Any | None = None,
|
|
164
|
-
json: dict | None = None,
|
|
165
|
-
**kwargs,
|
|
166
|
-
) -> requests.Response:
|
|
167
|
-
global USE_SYSTEM_CERTS
|
|
168
|
-
|
|
169
|
-
_log_debug_request(
|
|
170
|
-
"POST",
|
|
171
|
-
url,
|
|
172
|
-
json=json,
|
|
173
|
-
params=kwargs.get("params"),
|
|
174
|
-
headers=kwargs.get("headers"),
|
|
175
|
-
timeout=kwargs.get("timeout"),
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
if USE_SYSTEM_CERTS:
|
|
179
|
-
with requests.Session() as session:
|
|
180
|
-
session.mount("https://", HTTPSystemCertsAdapter())
|
|
181
|
-
resp = session.post(url, data=data, json=json, **kwargs)
|
|
182
39
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if "SSLCertVerificationError" not in str(ex):
|
|
188
|
-
raise ex
|
|
189
|
-
USE_SYSTEM_CERTS = True
|
|
190
|
-
# HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
|
|
191
|
-
LOG.warning(
|
|
192
|
-
"SSL error occurred, falling back to system SSL certificates: %s", ex
|
|
193
|
-
)
|
|
194
|
-
return request_post(url, data=data, json=json, **kwargs)
|
|
195
|
-
|
|
196
|
-
_log_debug_response(resp)
|
|
40
|
+
def create_user_session(user_access_token: str) -> requests.Session:
|
|
41
|
+
session = http.Session()
|
|
42
|
+
session.headers["Authorization"] = f"OAuth {user_access_token}"
|
|
43
|
+
return session
|
|
197
44
|
|
|
198
|
-
return resp
|
|
199
45
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
_log_debug_request(
|
|
209
|
-
"GET",
|
|
210
|
-
url,
|
|
211
|
-
params=kwargs.get("params"),
|
|
212
|
-
headers=kwargs.get("headers"),
|
|
213
|
-
timeout=kwargs.get("timeout"),
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
if USE_SYSTEM_CERTS:
|
|
217
|
-
with requests.Session() as session:
|
|
218
|
-
session.mount("https://", HTTPSystemCertsAdapter())
|
|
219
|
-
resp = session.get(url, params=params, **kwargs)
|
|
220
|
-
else:
|
|
221
|
-
try:
|
|
222
|
-
resp = requests.get(url, params=params, **kwargs)
|
|
223
|
-
except requests.exceptions.SSLError as ex:
|
|
224
|
-
if "SSLCertVerificationError" not in str(ex):
|
|
225
|
-
raise ex
|
|
226
|
-
USE_SYSTEM_CERTS = True
|
|
227
|
-
# HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
|
|
228
|
-
LOG.warning(
|
|
229
|
-
"SSL error occurred, falling back to system SSL certificates: %s", ex
|
|
230
|
-
)
|
|
231
|
-
resp = request_get(url, params=params, **kwargs)
|
|
232
|
-
|
|
233
|
-
_log_debug_response(resp)
|
|
234
|
-
|
|
235
|
-
return resp
|
|
46
|
+
def create_client_session(disable_logging: bool = False) -> requests.Session:
|
|
47
|
+
session = http.Session()
|
|
48
|
+
session.headers["Authorization"] = f"OAuth {MAPILLARY_CLIENT_TOKEN}"
|
|
49
|
+
if disable_logging:
|
|
50
|
+
session.disable_logging_request = True
|
|
51
|
+
session.disable_logging_response = True
|
|
52
|
+
return session
|
|
236
53
|
|
|
237
54
|
|
|
238
55
|
def is_auth_error(resp: requests.Response) -> bool:
|
|
@@ -273,55 +90,42 @@ def extract_auth_error_message(resp: requests.Response) -> str:
|
|
|
273
90
|
return resp.text
|
|
274
91
|
|
|
275
92
|
|
|
276
|
-
def get_upload_token(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
)
|
|
93
|
+
def get_upload_token(
|
|
94
|
+
client_session: requests.Session, email: str, password: str
|
|
95
|
+
) -> requests.Response:
|
|
96
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/login"
|
|
97
|
+
json_data = {"email": email, "password": password, "locale": "en_US"}
|
|
98
|
+
|
|
99
|
+
resp = client_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
|
|
283
100
|
resp.raise_for_status()
|
|
101
|
+
|
|
284
102
|
return resp
|
|
285
103
|
|
|
286
104
|
|
|
287
105
|
def fetch_organization(
|
|
288
|
-
|
|
106
|
+
user_session: requests.Session, organization_id: int | str
|
|
289
107
|
) -> requests.Response:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
},
|
|
295
|
-
headers={
|
|
296
|
-
"Authorization": f"OAuth {user_access_token}",
|
|
297
|
-
},
|
|
298
|
-
timeout=REQUESTS_TIMEOUT,
|
|
299
|
-
)
|
|
108
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}"
|
|
109
|
+
params = {"fields": ",".join(["slug", "description", "name"])}
|
|
110
|
+
|
|
111
|
+
resp = user_session.get(url, params=params, timeout=REQUESTS_TIMEOUT)
|
|
300
112
|
resp.raise_for_status()
|
|
113
|
+
|
|
301
114
|
return resp
|
|
302
115
|
|
|
303
116
|
|
|
304
117
|
def fetch_user_or_me(
|
|
305
|
-
|
|
306
|
-
user_id: int | str | None = None,
|
|
118
|
+
user_session: requests.Session, user_id: int | str | None = None
|
|
307
119
|
) -> requests.Response:
|
|
308
120
|
if user_id is None:
|
|
309
121
|
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
|
|
310
122
|
else:
|
|
311
123
|
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
|
|
124
|
+
params = {"fields": ",".join(["id", "username"])}
|
|
312
125
|
|
|
313
|
-
resp =
|
|
314
|
-
url,
|
|
315
|
-
params={
|
|
316
|
-
"fields": ",".join(["id", "username"]),
|
|
317
|
-
},
|
|
318
|
-
headers={
|
|
319
|
-
"Authorization": f"OAuth {user_access_token}",
|
|
320
|
-
},
|
|
321
|
-
timeout=REQUESTS_TIMEOUT,
|
|
322
|
-
)
|
|
323
|
-
|
|
126
|
+
resp = user_session.get(url, params=params, timeout=REQUESTS_TIMEOUT)
|
|
324
127
|
resp.raise_for_status()
|
|
128
|
+
|
|
325
129
|
return resp
|
|
326
130
|
|
|
327
131
|
|
|
@@ -330,44 +134,43 @@ ActionType = T.Literal[
|
|
|
330
134
|
]
|
|
331
135
|
|
|
332
136
|
|
|
333
|
-
def log_event(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
headers={
|
|
341
|
-
"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}",
|
|
342
|
-
},
|
|
343
|
-
timeout=REQUESTS_TIMEOUT,
|
|
344
|
-
)
|
|
137
|
+
def log_event(
|
|
138
|
+
client_session: requests.Session, action_type: ActionType, properties: dict
|
|
139
|
+
) -> requests.Response:
|
|
140
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging"
|
|
141
|
+
json_data = {"action_type": action_type, "properties": properties}
|
|
142
|
+
|
|
143
|
+
resp = client_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
|
|
345
144
|
resp.raise_for_status()
|
|
145
|
+
|
|
346
146
|
return resp
|
|
347
147
|
|
|
348
148
|
|
|
349
149
|
def finish_upload(
|
|
350
|
-
|
|
150
|
+
user_session: requests.Session,
|
|
351
151
|
file_handle: str,
|
|
352
152
|
cluster_filetype: ClusterFileType,
|
|
353
153
|
organization_id: int | str | None = None,
|
|
354
154
|
) -> requests.Response:
|
|
355
|
-
|
|
155
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
|
|
156
|
+
json_data: dict[str, str | int] = {
|
|
356
157
|
"file_handle": file_handle,
|
|
357
158
|
"file_type": cluster_filetype.value,
|
|
358
159
|
}
|
|
359
160
|
if organization_id is not None:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
resp = request_post(
|
|
363
|
-
f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload",
|
|
364
|
-
headers={
|
|
365
|
-
"Authorization": f"OAuth {user_access_token}",
|
|
366
|
-
},
|
|
367
|
-
json=data,
|
|
368
|
-
timeout=REQUESTS_TIMEOUT,
|
|
369
|
-
)
|
|
161
|
+
json_data["organization_id"] = organization_id
|
|
370
162
|
|
|
163
|
+
resp = user_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
|
|
371
164
|
resp.raise_for_status()
|
|
372
165
|
|
|
373
166
|
return resp
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def jsonify_response(resp: requests.Response) -> T.Any:
|
|
170
|
+
"""
|
|
171
|
+
Convert the response to JSON, raising HTTPContentError if the response is not JSON.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
return resp.json()
|
|
175
|
+
except requests.JSONDecodeError as ex:
|
|
176
|
+
raise HTTPContentError("Invalid JSON response", resp) from ex
|
mapillary_tools/authenticate.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import getpass
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
import re
|
|
7
6
|
import sys
|
|
@@ -11,7 +10,7 @@ import jsonschema
|
|
|
11
10
|
|
|
12
11
|
import requests
|
|
13
12
|
|
|
14
|
-
from . import api_v4, config, constants, exceptions
|
|
13
|
+
from . import api_v4, config, constants, exceptions, http
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
LOG = logging.getLogger(__name__)
|
|
@@ -78,17 +77,16 @@ def authenticate(
|
|
|
78
77
|
# TODO: print more user information
|
|
79
78
|
if profile_name in all_user_items:
|
|
80
79
|
LOG.info(
|
|
81
|
-
'Profile "%s" updated: %s', profile_name,
|
|
80
|
+
'Profile "%s" updated: %s', profile_name, http._sanitize(user_items)
|
|
82
81
|
)
|
|
83
82
|
else:
|
|
84
83
|
LOG.info(
|
|
85
|
-
'Profile "%s" created: %s', profile_name,
|
|
84
|
+
'Profile "%s" created: %s', profile_name, http._sanitize(user_items)
|
|
86
85
|
)
|
|
87
86
|
|
|
88
87
|
|
|
89
88
|
def fetch_user_items(
|
|
90
|
-
user_name: str | None = None,
|
|
91
|
-
organization_key: str | None = None,
|
|
89
|
+
user_name: str | None = None, organization_key: str | None = None
|
|
92
90
|
) -> config.UserItem:
|
|
93
91
|
"""
|
|
94
92
|
Read user information from the config file,
|
|
@@ -129,18 +127,28 @@ def fetch_user_items(
|
|
|
129
127
|
|
|
130
128
|
assert profile_name is not None, "profile_name should be set"
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'Uploading to profile "%s": %s', profile_name, api_v4._sanitize(user_items)
|
|
136
|
-
)
|
|
130
|
+
try:
|
|
131
|
+
LOG.info(f'Verifying profile "{profile_name}"...')
|
|
132
|
+
user_items = _verify_user_auth(_validate_profile(user_items))
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
user_items["user_upload_token"], organization_key
|
|
134
|
+
LOG.info(
|
|
135
|
+
f'Uploading to profile "{profile_name}": {user_items.get("MAPSettingsUsername")} (ID: {user_items.get("MAPSettingsUserKey")})'
|
|
141
136
|
)
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
|
|
138
|
+
if organization_key is not None:
|
|
139
|
+
with api_v4.create_user_session(user_items["user_upload_token"]) as session:
|
|
140
|
+
resp = api_v4.fetch_organization(session, organization_key)
|
|
141
|
+
data = api_v4.jsonify_response(resp)
|
|
142
|
+
LOG.info(
|
|
143
|
+
f"Uploading to organization: {data.get('name')} (ID: {data.get('id')})"
|
|
144
|
+
)
|
|
145
|
+
user_items["MAPOrganizationKey"] = data.get("id")
|
|
146
|
+
|
|
147
|
+
except requests.Timeout as ex:
|
|
148
|
+
raise exceptions.MapillaryUploadTimeoutError(str(ex)) from ex
|
|
149
|
+
|
|
150
|
+
except requests.ConnectionError as ex:
|
|
151
|
+
raise exceptions.MapillaryUploadConnectionError(str(ex)) from ex
|
|
144
152
|
|
|
145
153
|
return user_items
|
|
146
154
|
|
|
@@ -172,23 +180,22 @@ def _verify_user_auth(user_items: config.UserItem) -> config.UserItem:
|
|
|
172
180
|
if constants._AUTH_VERIFICATION_DISABLED:
|
|
173
181
|
return user_items
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
raise ex
|
|
183
|
+
with api_v4.create_user_session(user_items["user_upload_token"]) as session:
|
|
184
|
+
try:
|
|
185
|
+
resp = api_v4.fetch_user_or_me(session)
|
|
186
|
+
except requests.HTTPError as ex:
|
|
187
|
+
if api_v4.is_auth_error(ex.response):
|
|
188
|
+
message = api_v4.extract_auth_error_message(ex.response)
|
|
189
|
+
raise exceptions.MapillaryUploadUnauthorizedError(message)
|
|
190
|
+
else:
|
|
191
|
+
raise ex
|
|
185
192
|
|
|
186
|
-
|
|
193
|
+
data = api_v4.jsonify_response(resp)
|
|
187
194
|
|
|
188
195
|
return {
|
|
189
196
|
**user_items,
|
|
190
|
-
"MAPSettingsUsername":
|
|
191
|
-
"MAPSettingsUserKey":
|
|
197
|
+
"MAPSettingsUsername": data.get("username"),
|
|
198
|
+
"MAPSettingsUserKey": data.get("id"),
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
|
|
@@ -275,18 +282,19 @@ def _prompt_login(
|
|
|
275
282
|
if user_password:
|
|
276
283
|
break
|
|
277
284
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
with api_v4.create_client_session() as session:
|
|
286
|
+
try:
|
|
287
|
+
resp = api_v4.get_upload_token(session, user_email, user_password)
|
|
288
|
+
except requests.HTTPError as ex:
|
|
289
|
+
if not _enabled:
|
|
290
|
+
raise ex
|
|
283
291
|
|
|
284
|
-
|
|
285
|
-
|
|
292
|
+
if _is_login_retryable(ex):
|
|
293
|
+
return _prompt_login()
|
|
286
294
|
|
|
287
|
-
|
|
295
|
+
raise ex
|
|
288
296
|
|
|
289
|
-
data =
|
|
297
|
+
data = api_v4.jsonify_response(resp)
|
|
290
298
|
|
|
291
299
|
user_items: config.UserItem = {
|
|
292
300
|
"user_upload_token": str(data["access_token"]),
|
|
@@ -7,6 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
9
9
|
from .. import api_v4, constants, exceptions, VERSION
|
|
10
|
+
from ..upload import log_exception
|
|
11
|
+
from ..utils import configure_logger, get_app_name
|
|
10
12
|
from . import (
|
|
11
13
|
authenticate,
|
|
12
14
|
process,
|
|
@@ -30,8 +32,8 @@ mapillary_tools_commands = [
|
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
#
|
|
34
|
-
LOG = logging.getLogger(
|
|
35
|
+
# Root logger of mapillary_tools (not including third-party libraries)
|
|
36
|
+
LOG = logging.getLogger(get_app_name())
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
# Handle shared arguments/options here
|
|
@@ -78,13 +80,6 @@ def add_general_arguments(parser, command):
|
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
|
|
81
|
-
def configure_logger(logger: logging.Logger, stream=None) -> None:
|
|
82
|
-
formatter = logging.Formatter("%(asctime)s - %(levelname)-7s - %(message)s")
|
|
83
|
-
handler = logging.StreamHandler(stream)
|
|
84
|
-
handler.setFormatter(formatter)
|
|
85
|
-
logger.addHandler(handler)
|
|
86
|
-
|
|
87
|
-
|
|
88
83
|
def _log_params(argvars: dict) -> None:
|
|
89
84
|
MAX_ENTRIES = 5
|
|
90
85
|
|
|
@@ -151,9 +146,7 @@ def main():
|
|
|
151
146
|
|
|
152
147
|
args = parser.parse_args()
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
configure_logger(LOG, sys.stderr)
|
|
156
|
-
LOG.setLevel(log_level)
|
|
149
|
+
configure_logger(LOG, level=logging.DEBUG if args.verbose else logging.INFO)
|
|
157
150
|
|
|
158
151
|
LOG.debug("%s", version_text)
|
|
159
152
|
argvars = vars(args)
|
|
@@ -162,16 +155,22 @@ def main():
|
|
|
162
155
|
try:
|
|
163
156
|
args.func(argvars)
|
|
164
157
|
except requests.HTTPError as ex:
|
|
165
|
-
|
|
158
|
+
log_exception(ex)
|
|
166
159
|
# TODO: standardize exit codes as exceptions.MapillaryUserError
|
|
167
160
|
sys.exit(16)
|
|
168
161
|
|
|
162
|
+
except api_v4.HTTPContentError as ex:
|
|
163
|
+
log_exception(ex)
|
|
164
|
+
sys.exit(17)
|
|
165
|
+
|
|
169
166
|
except exceptions.MapillaryUserError as ex:
|
|
170
|
-
|
|
171
|
-
"%s: %s", ex.__class__.__name__, ex, exc_info=log_level == logging.DEBUG
|
|
172
|
-
)
|
|
167
|
+
log_exception(ex)
|
|
173
168
|
sys.exit(ex.exit_code)
|
|
174
169
|
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
LOG.info("Interrupted by user...")
|
|
172
|
+
sys.exit(130)
|
|
173
|
+
|
|
175
174
|
|
|
176
175
|
if __name__ == "__main__":
|
|
177
176
|
main()
|
|
@@ -8,13 +8,13 @@ from .process import bold_text
|
|
|
8
8
|
|
|
9
9
|
class Command:
|
|
10
10
|
name = "upload"
|
|
11
|
-
help = "
|
|
11
|
+
help = "Upload processed data to Mapillary"
|
|
12
12
|
|
|
13
13
|
@staticmethod
|
|
14
14
|
def add_common_upload_options(group):
|
|
15
15
|
group.add_argument(
|
|
16
16
|
"--user_name",
|
|
17
|
-
help="The Mapillary user account to upload to.
|
|
17
|
+
help="The Mapillary user account to upload to.",
|
|
18
18
|
required=False,
|
|
19
19
|
)
|
|
20
20
|
group.add_argument(
|
|
@@ -23,9 +23,38 @@ class Command:
|
|
|
23
23
|
default=None,
|
|
24
24
|
required=False,
|
|
25
25
|
)
|
|
26
|
+
group.add_argument(
|
|
27
|
+
"--num_upload_workers",
|
|
28
|
+
help="Number of concurrent upload workers for uploading images. [default: %(default)s]",
|
|
29
|
+
default=constants.MAX_IMAGE_UPLOAD_WORKERS,
|
|
30
|
+
type=int,
|
|
31
|
+
required=False,
|
|
32
|
+
)
|
|
33
|
+
group.add_argument(
|
|
34
|
+
"--reupload",
|
|
35
|
+
help="Re-upload data that has already been uploaded.",
|
|
36
|
+
action="store_true",
|
|
37
|
+
default=False,
|
|
38
|
+
required=False,
|
|
39
|
+
)
|
|
26
40
|
group.add_argument(
|
|
27
41
|
"--dry_run",
|
|
28
|
-
|
|
42
|
+
"--dryrun",
|
|
43
|
+
help="[DEVELOPMENT] Simulate upload by sending data to a local directory instead of Mapillary servers. Uses a temporary directory by default unless specified by MAPILLARY_UPLOAD_ENDPOINT environment variable.",
|
|
44
|
+
action="store_true",
|
|
45
|
+
default=False,
|
|
46
|
+
required=False,
|
|
47
|
+
)
|
|
48
|
+
group.add_argument(
|
|
49
|
+
"--nofinish",
|
|
50
|
+
help="[DEVELOPMENT] Upload data without finalizing. The data will NOT be stored permanently or appear on the Mapillary website.",
|
|
51
|
+
action="store_true",
|
|
52
|
+
default=False,
|
|
53
|
+
required=False,
|
|
54
|
+
)
|
|
55
|
+
group.add_argument(
|
|
56
|
+
"--noresume",
|
|
57
|
+
help="[DEVELOPMENT] Start upload from the beginning, ignoring any previously interrupted upload sessions.",
|
|
29
58
|
action="store_true",
|
|
30
59
|
default=False,
|
|
31
60
|
required=False,
|
|
@@ -35,7 +64,7 @@ class Command:
|
|
|
35
64
|
group = parser.add_argument_group(bold_text("UPLOAD OPTIONS"))
|
|
36
65
|
group.add_argument(
|
|
37
66
|
"--desc_path",
|
|
38
|
-
help=f'Path to the description file
|
|
67
|
+
help=f'Path to the description file with processed image and video metadata (from process command). Use "-" for STDIN. [default: {{IMPORT_PATH}}/{constants.IMAGE_DESCRIPTION_FILENAME}]',
|
|
39
68
|
default=None,
|
|
40
69
|
required=False,
|
|
41
70
|
)
|