mapillary-tools 0.14.0a2__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 (49) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -262
  3. mapillary_tools/authenticate.py +54 -46
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/commands/__main__.py +15 -16
  6. mapillary_tools/commands/upload.py +33 -4
  7. mapillary_tools/config.py +38 -17
  8. mapillary_tools/constants.py +127 -43
  9. mapillary_tools/exceptions.py +4 -0
  10. mapillary_tools/exif_read.py +2 -1
  11. mapillary_tools/exif_write.py +3 -1
  12. mapillary_tools/exiftool_read_video.py +52 -15
  13. mapillary_tools/exiftool_runner.py +4 -24
  14. mapillary_tools/ffmpeg.py +406 -232
  15. mapillary_tools/geo.py +16 -0
  16. mapillary_tools/geotag/__init__.py +0 -0
  17. mapillary_tools/geotag/base.py +8 -4
  18. mapillary_tools/geotag/factory.py +106 -89
  19. mapillary_tools/geotag/geotag_images_from_exiftool.py +27 -20
  20. mapillary_tools/geotag/geotag_images_from_gpx.py +7 -6
  21. mapillary_tools/geotag/geotag_images_from_video.py +35 -0
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +61 -14
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
  24. mapillary_tools/geotag/options.py +25 -3
  25. mapillary_tools/geotag/utils.py +9 -12
  26. mapillary_tools/geotag/video_extractors/base.py +1 -1
  27. mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
  28. mapillary_tools/geotag/video_extractors/gpx.py +61 -70
  29. mapillary_tools/geotag/video_extractors/native.py +34 -31
  30. mapillary_tools/history.py +128 -8
  31. mapillary_tools/http.py +211 -0
  32. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  33. mapillary_tools/process_geotag_properties.py +47 -35
  34. mapillary_tools/process_sequence_properties.py +340 -325
  35. mapillary_tools/sample_video.py +8 -8
  36. mapillary_tools/serializer/description.py +587 -0
  37. mapillary_tools/serializer/gpx.py +132 -0
  38. mapillary_tools/types.py +44 -610
  39. mapillary_tools/upload.py +327 -352
  40. mapillary_tools/upload_api_v4.py +125 -72
  41. mapillary_tools/uploader.py +797 -216
  42. mapillary_tools/utils.py +57 -5
  43. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +91 -34
  44. mapillary_tools-0.14.1.dist-info/RECORD +76 -0
  45. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +1 -1
  46. mapillary_tools-0.14.0a2.dist-info/RECORD +0 -72
  47. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
  48. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
  49. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.14.0a2"
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,221 +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"
19
+ REQUESTS_TIMEOUT: float = 60 # 1 minutes
28
20
 
29
21
 
30
- class HTTPSystemCertsAdapter(HTTPAdapter):
22
+ class HTTPContentError(Exception):
31
23
  """
32
- This adapter uses the system's certificate store instead of the certifi module.
33
-
34
- The implementation is based on the project https://pypi.org/project/pip-system-certs/,
35
- 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.
36
26
  """
37
27
 
38
- def init_poolmanager(self, *args, **kwargs):
39
- ssl_context = ssl.create_default_context()
40
- ssl_context.load_default_certs()
41
- kwargs["ssl_context"] = ssl_context
42
-
43
- super().init_poolmanager(*args, **kwargs)
44
-
45
- def cert_verify(self, *args, **kwargs):
46
- super().cert_verify(*args, **kwargs)
47
-
48
- # By default Python requests uses the ca_certs from the certifi module
49
- # But we want to use the certificate store instead.
50
- # By clearing the ca_certs variable we force it to fall back on that behaviour (handled in urllib3)
51
- if "conn" in kwargs:
52
- conn = kwargs["conn"]
53
- else:
54
- conn = args[0]
55
-
56
- conn.ca_certs = None
57
-
58
-
59
- @T.overload
60
- def _truncate(s: bytes, limit: int = 512) -> bytes: ...
61
-
62
-
63
- @T.overload
64
- def _truncate(s: str, limit: int = 512) -> str: ...
65
-
66
-
67
- def _truncate(s, limit=512):
68
- if limit < len(s):
69
- remaining = len(s) - limit
70
- if isinstance(s, bytes):
71
- return (
72
- s[:limit]
73
- + b"..."
74
- + f"({remaining} more bytes truncated)".encode("utf-8")
75
- )
76
- else:
77
- return str(s[:limit]) + f"...({remaining} more chars truncated)"
78
- else:
79
- return s
80
-
81
-
82
- def _sanitize(headers: T.Mapping[T.Any, T.Any]) -> T.Mapping[T.Any, T.Any]:
83
- new_headers = {}
84
-
85
- for k, v in headers.items():
86
- if k.lower() in [
87
- "authorization",
88
- "cookie",
89
- "x-fb-access-token",
90
- "access-token",
91
- "access_token",
92
- "password",
93
- "user_upload_token",
94
- ]:
95
- new_headers[k] = "[REDACTED]"
96
- else:
97
- new_headers[k] = _truncate(v)
98
-
99
- return new_headers
100
-
101
-
102
- def _log_debug_request(
103
- method: str,
104
- url: str,
105
- json: dict | None = None,
106
- params: dict | None = None,
107
- headers: dict | None = None,
108
- timeout: T.Any = None,
109
- ):
110
- if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
111
- return
112
-
113
- msg = f"HTTP {method} {url}"
114
-
115
- if USE_SYSTEM_CERTS:
116
- msg += " (w/sys_certs)"
117
-
118
- if json:
119
- t = _truncate(dumps(_sanitize(json)))
120
- msg += f" JSON={t}"
121
-
122
- if params:
123
- msg += f" PARAMS={_sanitize(params)}"
124
-
125
- if headers:
126
- msg += f" HEADERS={_sanitize(headers)}"
127
-
128
- if timeout is not None:
129
- msg += f" TIMEOUT={timeout}"
130
-
131
- LOG.debug(msg)
132
-
133
-
134
- def _log_debug_response(resp: requests.Response):
135
- if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
136
- return
137
-
138
- data: str | bytes
139
- try:
140
- data = _truncate(dumps(_sanitize(resp.json())))
141
- except Exception:
142
- data = _truncate(resp.content)
28
+ def __init__(self, message: str, response: requests.Response):
29
+ self.response = response
30
+ super().__init__(message)
143
31
 
144
- LOG.debug(f"HTTP {resp.status_code} ({resp.reason}): %s", data)
145
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"
146
38
 
147
- def readable_http_error(ex: requests.HTTPError) -> str:
148
- req = ex.request
149
- resp = ex.response
150
-
151
- data: str | bytes
152
- try:
153
- data = _truncate(dumps(_sanitize(resp.json())))
154
- except Exception:
155
- data = _truncate(resp.content)
156
-
157
- return f"{req.method} {resp.url} => {resp.status_code} ({resp.reason}): {str(data)}"
158
-
159
-
160
- def request_post(
161
- url: str,
162
- data: T.Any | None = None,
163
- json: dict | None = None,
164
- **kwargs,
165
- ) -> requests.Response:
166
- global USE_SYSTEM_CERTS
167
-
168
- _log_debug_request(
169
- "POST",
170
- url,
171
- json=json,
172
- params=kwargs.get("params"),
173
- headers=kwargs.get("headers"),
174
- timeout=kwargs.get("timeout"),
175
- )
176
-
177
- if USE_SYSTEM_CERTS:
178
- with requests.Session() as session:
179
- session.mount("https://", HTTPSystemCertsAdapter())
180
- resp = session.post(url, data=data, json=json, **kwargs)
181
-
182
- else:
183
- try:
184
- resp = requests.post(url, data=data, json=json, **kwargs)
185
- except requests.exceptions.SSLError as ex:
186
- if "SSLCertVerificationError" not in str(ex):
187
- raise ex
188
- USE_SYSTEM_CERTS = True
189
- # 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)')))
190
- LOG.warning(
191
- "SSL error occurred, falling back to system SSL certificates: %s", ex
192
- )
193
- return request_post(url, data=data, json=json, **kwargs)
194
-
195
- _log_debug_response(resp)
196
-
197
- return resp
198
39
 
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
199
44
 
200
- def request_get(
201
- url: str,
202
- params: dict | None = None,
203
- **kwargs,
204
- ) -> requests.Response:
205
- global USE_SYSTEM_CERTS
206
-
207
- _log_debug_request(
208
- "GET",
209
- url,
210
- params=kwargs.get("params"),
211
- headers=kwargs.get("headers"),
212
- timeout=kwargs.get("timeout"),
213
- )
214
-
215
- if USE_SYSTEM_CERTS:
216
- with requests.Session() as session:
217
- session.mount("https://", HTTPSystemCertsAdapter())
218
- resp = session.get(url, params=params, **kwargs)
219
- else:
220
- try:
221
- resp = requests.get(url, params=params, **kwargs)
222
- except requests.exceptions.SSLError as ex:
223
- if "SSLCertVerificationError" not in str(ex):
224
- raise ex
225
- USE_SYSTEM_CERTS = True
226
- # 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)')))
227
- LOG.warning(
228
- "SSL error occurred, falling back to system SSL certificates: %s", ex
229
- )
230
- resp = request_get(url, params=params, **kwargs)
231
-
232
- _log_debug_response(resp)
233
45
 
234
- 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
235
53
 
236
54
 
237
55
  def is_auth_error(resp: requests.Response) -> bool:
@@ -272,55 +90,42 @@ def extract_auth_error_message(resp: requests.Response) -> str:
272
90
  return resp.text
273
91
 
274
92
 
275
- def get_upload_token(email: str, password: str) -> requests.Response:
276
- resp = request_post(
277
- f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
278
- headers={"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}"},
279
- json={"email": email, "password": password, "locale": "en_US"},
280
- timeout=REQUESTS_TIMEOUT,
281
- )
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)
282
100
  resp.raise_for_status()
101
+
283
102
  return resp
284
103
 
285
104
 
286
105
  def fetch_organization(
287
- user_access_token: str, organization_id: int | str
106
+ user_session: requests.Session, organization_id: int | str
288
107
  ) -> requests.Response:
289
- resp = request_get(
290
- f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
291
- params={
292
- "fields": ",".join(["slug", "description", "name"]),
293
- },
294
- headers={
295
- "Authorization": f"OAuth {user_access_token}",
296
- },
297
- timeout=REQUESTS_TIMEOUT,
298
- )
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)
299
112
  resp.raise_for_status()
113
+
300
114
  return resp
301
115
 
302
116
 
303
117
  def fetch_user_or_me(
304
- user_access_token: str,
305
- user_id: int | str | None = None,
118
+ user_session: requests.Session, user_id: int | str | None = None
306
119
  ) -> requests.Response:
307
120
  if user_id is None:
308
121
  url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
309
122
  else:
310
123
  url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
124
+ params = {"fields": ",".join(["id", "username"])}
311
125
 
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
-
126
+ resp = user_session.get(url, params=params, timeout=REQUESTS_TIMEOUT)
323
127
  resp.raise_for_status()
128
+
324
129
  return resp
325
130
 
326
131
 
@@ -329,44 +134,43 @@ ActionType = T.Literal[
329
134
  ]
330
135
 
331
136
 
332
- def log_event(action_type: ActionType, properties: dict) -> requests.Response:
333
- resp = request_post(
334
- f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
335
- json={
336
- "action_type": action_type,
337
- "properties": properties,
338
- },
339
- headers={
340
- "Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}",
341
- },
342
- timeout=REQUESTS_TIMEOUT,
343
- )
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)
344
144
  resp.raise_for_status()
145
+
345
146
  return resp
346
147
 
347
148
 
348
149
  def finish_upload(
349
- user_access_token: str,
150
+ user_session: requests.Session,
350
151
  file_handle: str,
351
152
  cluster_filetype: ClusterFileType,
352
153
  organization_id: int | str | None = None,
353
154
  ) -> requests.Response:
354
- data: dict[str, str | int] = {
155
+ url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
156
+ json_data: dict[str, str | int] = {
355
157
  "file_handle": file_handle,
356
158
  "file_type": cluster_filetype.value,
357
159
  }
358
160
  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
- )
161
+ json_data["organization_id"] = organization_id
369
162
 
163
+ resp = user_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
370
164
  resp.raise_for_status()
371
165
 
372
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, types
13
+ from . import api_v4, config, constants, exceptions, http
15
14
 
16
15
 
17
16
  LOG = logging.getLogger(__name__)
@@ -64,7 +63,7 @@ def authenticate(
64
63
  LOG.info('Creating new profile: "%s"', profile_name)
65
64
 
66
65
  if jwt:
67
- user_items: types.UserItem = {"user_upload_token": jwt}
66
+ user_items: config.UserItem = {"user_upload_token": jwt}
68
67
  user_items = _verify_user_auth(_validate_profile(user_items))
69
68
  else:
70
69
  user_items = _prompt_login(
@@ -78,18 +77,17 @@ 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,
92
- ) -> types.UserItem:
89
+ user_name: str | None = None, organization_key: str | None = None
90
+ ) -> config.UserItem:
93
91
  """
94
92
  Read user information from the config file,
95
93
  or prompt the user to authenticate if the specified profile does not exist
@@ -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
 
@@ -155,9 +163,9 @@ def _prompt(message: str) -> str:
155
163
  return input()
156
164
 
157
165
 
158
- def _validate_profile(user_items: types.UserItem) -> types.UserItem:
166
+ def _validate_profile(user_items: config.UserItem) -> config.UserItem:
159
167
  try:
160
- jsonschema.validate(user_items, types.UserItemSchema)
168
+ jsonschema.validate(user_items, config.UserItemSchema)
161
169
  except jsonschema.ValidationError as ex:
162
170
  raise exceptions.MapillaryBadParameterError(
163
171
  f"Invalid profile format: {ex.message}"
@@ -165,30 +173,29 @@ def _validate_profile(user_items: types.UserItem) -> types.UserItem:
165
173
  return user_items
166
174
 
167
175
 
168
- def _verify_user_auth(user_items: types.UserItem) -> types.UserItem:
176
+ def _verify_user_auth(user_items: config.UserItem) -> config.UserItem:
169
177
  """
170
178
  Verify that the user access token is valid
171
179
  """
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
 
@@ -205,7 +212,7 @@ def _validate_profile_name(profile_name: str):
205
212
  )
206
213
 
207
214
 
208
- def _list_all_profiles(profiles: dict[str, types.UserItem]) -> None:
215
+ def _list_all_profiles(profiles: dict[str, config.UserItem]) -> None:
209
216
  _echo("Existing Mapillary profiles:")
210
217
 
211
218
  # Header
@@ -256,7 +263,7 @@ def _is_login_retryable(ex: requests.HTTPError) -> bool:
256
263
  def _prompt_login(
257
264
  user_email: str | None = None,
258
265
  user_password: str | None = None,
259
- ) -> types.UserItem:
266
+ ) -> config.UserItem:
260
267
  _enabled = _prompt_enabled()
261
268
 
262
269
  if user_email is None:
@@ -275,20 +282,21 @@ 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
- user_items: types.UserItem = {
299
+ user_items: config.UserItem = {
292
300
  "user_upload_token": str(data["access_token"]),
293
301
  "MAPSettingsUserKey": str(data["user_id"]),
294
302
  }