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.
Files changed (36) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -263
  3. mapillary_tools/authenticate.py +46 -38
  4. mapillary_tools/commands/__main__.py +15 -16
  5. mapillary_tools/commands/upload.py +33 -4
  6. mapillary_tools/constants.py +127 -45
  7. mapillary_tools/exceptions.py +4 -0
  8. mapillary_tools/exif_read.py +2 -1
  9. mapillary_tools/exif_write.py +3 -1
  10. mapillary_tools/geo.py +16 -0
  11. mapillary_tools/geotag/base.py +6 -2
  12. mapillary_tools/geotag/factory.py +9 -1
  13. mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
  14. mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
  15. mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
  16. mapillary_tools/geotag/utils.py +9 -12
  17. mapillary_tools/geotag/video_extractors/gpx.py +2 -1
  18. mapillary_tools/geotag/video_extractors/native.py +25 -0
  19. mapillary_tools/history.py +124 -7
  20. mapillary_tools/http.py +211 -0
  21. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  22. mapillary_tools/process_geotag_properties.py +31 -27
  23. mapillary_tools/process_sequence_properties.py +339 -322
  24. mapillary_tools/sample_video.py +1 -2
  25. mapillary_tools/serializer/description.py +56 -56
  26. mapillary_tools/serializer/gpx.py +1 -1
  27. mapillary_tools/upload.py +201 -205
  28. mapillary_tools/upload_api_v4.py +57 -47
  29. mapillary_tools/uploader.py +720 -285
  30. mapillary_tools/utils.py +57 -5
  31. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +7 -6
  32. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/RECORD +36 -35
  33. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +0 -0
  34. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
  35. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
  36. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.14.0b1"
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
- from requests.adapters import HTTPAdapter
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 HTTPSystemCertsAdapter(HTTPAdapter):
22
+ class HTTPContentError(Exception):
32
23
  """
33
- This adapter uses the system's certificate store instead of the certifi module.
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 init_poolmanager(self, *args, **kwargs):
40
- ssl_context = ssl.create_default_context()
41
- ssl_context.load_default_certs()
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
- else:
184
- try:
185
- resp = requests.post(url, data=data, json=json, **kwargs)
186
- except requests.exceptions.SSLError as ex:
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
- def request_get(
202
- url: str,
203
- params: dict | None = None,
204
- **kwargs,
205
- ) -> requests.Response:
206
- global USE_SYSTEM_CERTS
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(email: str, password: str) -> requests.Response:
277
- resp = request_post(
278
- f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
279
- headers={"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}"},
280
- json={"email": email, "password": password, "locale": "en_US"},
281
- timeout=REQUESTS_TIMEOUT,
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
- user_access_token: str, organization_id: int | str
106
+ user_session: requests.Session, organization_id: int | str
289
107
  ) -> requests.Response:
290
- resp = request_get(
291
- f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
292
- params={
293
- "fields": ",".join(["slug", "description", "name"]),
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
- user_access_token: str,
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 = request_get(
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(action_type: ActionType, properties: dict) -> requests.Response:
334
- resp = request_post(
335
- f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
336
- json={
337
- "action_type": action_type,
338
- "properties": properties,
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
- user_access_token: str,
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
- data: dict[str, str | int] = {
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
- data["organization_id"] = organization_id
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
@@ -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, api_v4._sanitize(user_items)
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, api_v4._sanitize(user_items)
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
- 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
- )
130
+ try:
131
+ LOG.info(f'Verifying profile "{profile_name}"...')
132
+ user_items = _verify_user_auth(_validate_profile(user_items))
137
133
 
138
- if organization_key is not None:
139
- resp = api_v4.fetch_organization(
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
- LOG.info("Uploading to Mapillary organization: %s", json.dumps(resp.json()))
143
- user_items["MAPOrganizationKey"] = organization_key
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
- 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
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
- user_json = resp.json()
193
+ data = api_v4.jsonify_response(resp)
187
194
 
188
195
  return {
189
196
  **user_items,
190
- "MAPSettingsUsername": user_json.get("username"),
191
- "MAPSettingsUserKey": user_json.get("id"),
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
- 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
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
- if _is_login_retryable(ex):
285
- return _prompt_login()
292
+ if _is_login_retryable(ex):
293
+ return _prompt_login()
286
294
 
287
- raise ex
295
+ raise ex
288
296
 
289
- data = resp.json()
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
- # do not use __name__ here is because if you run tools as a module, __name__ will be "__main__"
34
- LOG = logging.getLogger("mapillary_tools")
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
- log_level = logging.DEBUG if args.verbose else logging.INFO
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
- LOG.error("%s: %s", ex.__class__.__name__, api_v4.readable_http_error(ex))
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
- LOG.error(
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 = "upload images to Mapillary"
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. If you only have one account authorized, it will upload to that account by default.",
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
- help='Instead of uploading to the Mapillary server, simulate uploading to the local directory "mapillary_public_uploads" for debugging purposes.',
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 generated by the process command. The hyphen "-" indicates STDIN. [default: {{IMPORT_PATH}}/{constants.IMAGE_DESCRIPTION_FILENAME}]',
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
  )