mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0__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 +287 -22
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +17 -8
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +182 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +408 -416
- mapillary_tools/upload_api_v4.py +172 -174
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
mapillary_tools/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.
|
|
1
|
+
VERSION = "0.14.0"
|
mapillary_tools/api_v4.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
1
4
|
import logging
|
|
2
5
|
import os
|
|
3
6
|
import ssl
|
|
4
7
|
import typing as T
|
|
8
|
+
from json import dumps
|
|
5
9
|
|
|
6
10
|
import requests
|
|
7
11
|
from requests.adapters import HTTPAdapter
|
|
@@ -17,6 +21,24 @@ REQUESTS_TIMEOUT = 60 # 1 minutes
|
|
|
17
21
|
USE_SYSTEM_CERTS: bool = False
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
class HTTPContentError(Exception):
|
|
25
|
+
"""
|
|
26
|
+
Raised when the HTTP response is ok (200) but the content is not as expected
|
|
27
|
+
e.g. not JSON or not a valid response.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, response: requests.Response):
|
|
31
|
+
self.response = response
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClusterFileType(enum.Enum):
|
|
36
|
+
ZIP = "zip"
|
|
37
|
+
BLACKVUE = "mly_blackvue_video"
|
|
38
|
+
CAMM = "mly_camm_video"
|
|
39
|
+
MLY_BUNDLE_MANIFEST = "mly_bundle_manifest"
|
|
40
|
+
|
|
41
|
+
|
|
20
42
|
class HTTPSystemCertsAdapter(HTTPAdapter):
|
|
21
43
|
"""
|
|
22
44
|
This adapter uses the system's certificate store instead of the certifi module.
|
|
@@ -46,22 +68,155 @@ class HTTPSystemCertsAdapter(HTTPAdapter):
|
|
|
46
68
|
conn.ca_certs = None
|
|
47
69
|
|
|
48
70
|
|
|
71
|
+
@T.overload
|
|
72
|
+
def _truncate(s: bytes, limit: int = 256) -> bytes | str: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@T.overload
|
|
76
|
+
def _truncate(s: str, limit: int = 256) -> str: ...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _truncate(s, limit=256):
|
|
80
|
+
if limit < len(s):
|
|
81
|
+
if isinstance(s, bytes):
|
|
82
|
+
try:
|
|
83
|
+
s = s.decode("utf-8")
|
|
84
|
+
except UnicodeDecodeError:
|
|
85
|
+
pass
|
|
86
|
+
remaining = len(s) - limit
|
|
87
|
+
if isinstance(s, bytes):
|
|
88
|
+
return s[:limit] + f"...({remaining} bytes truncated)".encode("utf-8")
|
|
89
|
+
else:
|
|
90
|
+
return str(s[:limit]) + f"...({remaining} chars truncated)"
|
|
91
|
+
else:
|
|
92
|
+
return s
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _sanitize(headers: T.Mapping[T.Any, T.Any]) -> T.Mapping[T.Any, T.Any]:
|
|
96
|
+
new_headers = {}
|
|
97
|
+
|
|
98
|
+
for k, v in headers.items():
|
|
99
|
+
if k.lower() in [
|
|
100
|
+
"authorization",
|
|
101
|
+
"cookie",
|
|
102
|
+
"x-fb-access-token",
|
|
103
|
+
"access-token",
|
|
104
|
+
"access_token",
|
|
105
|
+
"password",
|
|
106
|
+
"user_upload_token",
|
|
107
|
+
]:
|
|
108
|
+
new_headers[k] = "[REDACTED]"
|
|
109
|
+
else:
|
|
110
|
+
if isinstance(v, (str, bytes)):
|
|
111
|
+
new_headers[k] = T.cast(T.Any, _truncate(v))
|
|
112
|
+
else:
|
|
113
|
+
new_headers[k] = v
|
|
114
|
+
|
|
115
|
+
return new_headers
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _log_debug_request(
|
|
119
|
+
method: str,
|
|
120
|
+
url: str,
|
|
121
|
+
json: dict | None = None,
|
|
122
|
+
params: dict | None = None,
|
|
123
|
+
headers: dict | None = None,
|
|
124
|
+
timeout: T.Any = None,
|
|
125
|
+
):
|
|
126
|
+
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
msg = f"HTTP {method} {url}"
|
|
130
|
+
|
|
131
|
+
if USE_SYSTEM_CERTS:
|
|
132
|
+
msg += " (w/sys_certs)"
|
|
133
|
+
|
|
134
|
+
if json:
|
|
135
|
+
t = _truncate(dumps(_sanitize(json)))
|
|
136
|
+
msg += f" JSON={t}"
|
|
137
|
+
|
|
138
|
+
if params:
|
|
139
|
+
msg += f" PARAMS={_sanitize(params)}"
|
|
140
|
+
|
|
141
|
+
if headers:
|
|
142
|
+
msg += f" HEADERS={_sanitize(headers)}"
|
|
143
|
+
|
|
144
|
+
if timeout is not None:
|
|
145
|
+
msg += f" TIMEOUT={timeout}"
|
|
146
|
+
|
|
147
|
+
msg = msg.replace("\n", "\\n")
|
|
148
|
+
|
|
149
|
+
LOG.debug(msg)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _log_debug_response(resp: requests.Response):
|
|
153
|
+
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
elapsed = resp.elapsed.total_seconds() * 1000 # Convert to milliseconds
|
|
157
|
+
msg = f"HTTP {resp.status_code} {resp.reason} ({elapsed:.0f} ms): {str(_truncate_response_content(resp))}"
|
|
158
|
+
|
|
159
|
+
LOG.debug(msg)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _truncate_response_content(resp: requests.Response) -> str | bytes:
|
|
163
|
+
try:
|
|
164
|
+
json_data = resp.json()
|
|
165
|
+
except requests.JSONDecodeError:
|
|
166
|
+
if resp.content is not None:
|
|
167
|
+
data = _truncate(resp.content)
|
|
168
|
+
else:
|
|
169
|
+
data = ""
|
|
170
|
+
else:
|
|
171
|
+
if isinstance(json_data, dict):
|
|
172
|
+
data = _truncate(dumps(_sanitize(json_data)))
|
|
173
|
+
else:
|
|
174
|
+
data = _truncate(str(json_data))
|
|
175
|
+
|
|
176
|
+
if isinstance(data, bytes):
|
|
177
|
+
return data.replace(b"\n", b"\\n")
|
|
178
|
+
|
|
179
|
+
elif isinstance(data, str):
|
|
180
|
+
return data.replace("\n", "\\n")
|
|
181
|
+
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def readable_http_error(ex: requests.HTTPError) -> str:
|
|
186
|
+
return readable_http_response(ex.response)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def readable_http_response(resp: requests.Response) -> str:
|
|
190
|
+
return f"{resp.request.method} {resp.url} => {resp.status_code} {resp.reason}: {str(_truncate_response_content(resp))}"
|
|
191
|
+
|
|
192
|
+
|
|
49
193
|
def request_post(
|
|
50
194
|
url: str,
|
|
51
|
-
data: T.
|
|
52
|
-
json:
|
|
195
|
+
data: T.Any | None = None,
|
|
196
|
+
json: dict | None = None,
|
|
197
|
+
disable_debug=False,
|
|
53
198
|
**kwargs,
|
|
54
199
|
) -> requests.Response:
|
|
55
200
|
global USE_SYSTEM_CERTS
|
|
56
201
|
|
|
202
|
+
if not disable_debug:
|
|
203
|
+
_log_debug_request(
|
|
204
|
+
"POST",
|
|
205
|
+
url,
|
|
206
|
+
json=json,
|
|
207
|
+
params=kwargs.get("params"),
|
|
208
|
+
headers=kwargs.get("headers"),
|
|
209
|
+
timeout=kwargs.get("timeout"),
|
|
210
|
+
)
|
|
211
|
+
|
|
57
212
|
if USE_SYSTEM_CERTS:
|
|
58
213
|
with requests.Session() as session:
|
|
59
214
|
session.mount("https://", HTTPSystemCertsAdapter())
|
|
60
|
-
|
|
215
|
+
resp = session.post(url, data=data, json=json, **kwargs)
|
|
61
216
|
|
|
62
217
|
else:
|
|
63
218
|
try:
|
|
64
|
-
|
|
219
|
+
resp = requests.post(url, data=data, json=json, **kwargs)
|
|
65
220
|
except requests.exceptions.SSLError as ex:
|
|
66
221
|
if "SSLCertVerificationError" not in str(ex):
|
|
67
222
|
raise ex
|
|
@@ -70,25 +225,36 @@ def request_post(
|
|
|
70
225
|
LOG.warning(
|
|
71
226
|
"SSL error occurred, falling back to system SSL certificates: %s", ex
|
|
72
227
|
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
228
|
+
return request_post(url, data=data, json=json, **kwargs)
|
|
229
|
+
|
|
230
|
+
if not disable_debug:
|
|
231
|
+
_log_debug_response(resp)
|
|
232
|
+
|
|
233
|
+
return resp
|
|
76
234
|
|
|
77
235
|
|
|
78
236
|
def request_get(
|
|
79
|
-
url: str,
|
|
80
|
-
params: T.Optional[dict] = None,
|
|
81
|
-
**kwargs,
|
|
237
|
+
url: str, params: dict | None = None, disable_debug=False, **kwargs
|
|
82
238
|
) -> requests.Response:
|
|
83
239
|
global USE_SYSTEM_CERTS
|
|
84
240
|
|
|
241
|
+
if not disable_debug:
|
|
242
|
+
_log_debug_request(
|
|
243
|
+
"GET",
|
|
244
|
+
url,
|
|
245
|
+
params=kwargs.get("params"),
|
|
246
|
+
headers=kwargs.get("headers"),
|
|
247
|
+
# Do not log timeout here as it's always set to REQUESTS_TIMEOUT
|
|
248
|
+
timeout=None,
|
|
249
|
+
)
|
|
250
|
+
|
|
85
251
|
if USE_SYSTEM_CERTS:
|
|
86
252
|
with requests.Session() as session:
|
|
87
253
|
session.mount("https://", HTTPSystemCertsAdapter())
|
|
88
|
-
|
|
254
|
+
resp = session.get(url, params=params, **kwargs)
|
|
89
255
|
else:
|
|
90
256
|
try:
|
|
91
|
-
|
|
257
|
+
resp = requests.get(url, params=params, **kwargs)
|
|
92
258
|
except requests.exceptions.SSLError as ex:
|
|
93
259
|
if "SSLCertVerificationError" not in str(ex):
|
|
94
260
|
raise ex
|
|
@@ -97,15 +263,56 @@ def request_get(
|
|
|
97
263
|
LOG.warning(
|
|
98
264
|
"SSL error occurred, falling back to system SSL certificates: %s", ex
|
|
99
265
|
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
266
|
+
resp = request_get(url, params=params, **kwargs)
|
|
267
|
+
|
|
268
|
+
if not disable_debug:
|
|
269
|
+
_log_debug_response(resp)
|
|
270
|
+
|
|
271
|
+
return resp
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def is_auth_error(resp: requests.Response) -> bool:
|
|
275
|
+
if resp.status_code in [401, 403]:
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
if resp.status_code in [400]:
|
|
279
|
+
try:
|
|
280
|
+
error_body = resp.json()
|
|
281
|
+
except Exception:
|
|
282
|
+
error_body = {}
|
|
283
|
+
|
|
284
|
+
type = error_body.get("debug_info", {}).get("type")
|
|
285
|
+
if type in ["NotAuthorizedError"]:
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def extract_auth_error_message(resp: requests.Response) -> str:
|
|
292
|
+
assert is_auth_error(resp), "has to be an auth error"
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
error_body = resp.json()
|
|
296
|
+
except Exception:
|
|
297
|
+
error_body = {}
|
|
298
|
+
|
|
299
|
+
# from Graph APIs
|
|
300
|
+
message = error_body.get("error", {}).get("message")
|
|
301
|
+
if message is not None:
|
|
302
|
+
return str(message)
|
|
303
|
+
|
|
304
|
+
# from upload service
|
|
305
|
+
message = error_body.get("debug_info", {}).get("message")
|
|
306
|
+
if message is not None:
|
|
307
|
+
return str(message)
|
|
308
|
+
|
|
309
|
+
return resp.text
|
|
103
310
|
|
|
104
311
|
|
|
105
312
|
def get_upload_token(email: str, password: str) -> requests.Response:
|
|
106
313
|
resp = request_post(
|
|
107
314
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
|
|
108
|
-
|
|
315
|
+
headers={"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}"},
|
|
109
316
|
json={"email": email, "password": password, "locale": "en_US"},
|
|
110
317
|
timeout=REQUESTS_TIMEOUT,
|
|
111
318
|
)
|
|
@@ -114,7 +321,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
|
|
|
114
321
|
|
|
115
322
|
|
|
116
323
|
def fetch_organization(
|
|
117
|
-
user_access_token: str, organization_id:
|
|
324
|
+
user_access_token: str, organization_id: int | str
|
|
118
325
|
) -> requests.Response:
|
|
119
326
|
resp = request_get(
|
|
120
327
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
|
|
@@ -130,22 +337,80 @@ def fetch_organization(
|
|
|
130
337
|
return resp
|
|
131
338
|
|
|
132
339
|
|
|
340
|
+
def fetch_user_or_me(
|
|
341
|
+
user_access_token: str, user_id: int | str | None = None
|
|
342
|
+
) -> requests.Response:
|
|
343
|
+
if user_id is None:
|
|
344
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
|
|
345
|
+
else:
|
|
346
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
|
|
347
|
+
|
|
348
|
+
resp = request_get(
|
|
349
|
+
url,
|
|
350
|
+
params={
|
|
351
|
+
"fields": ",".join(["id", "username"]),
|
|
352
|
+
},
|
|
353
|
+
headers={
|
|
354
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
355
|
+
},
|
|
356
|
+
timeout=REQUESTS_TIMEOUT,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
resp.raise_for_status()
|
|
360
|
+
return resp
|
|
361
|
+
|
|
362
|
+
|
|
133
363
|
ActionType = T.Literal[
|
|
134
364
|
"upload_started_upload", "upload_finished_upload", "upload_failed_upload"
|
|
135
365
|
]
|
|
136
366
|
|
|
137
367
|
|
|
138
|
-
def log_event(action_type: ActionType, properties:
|
|
368
|
+
def log_event(action_type: ActionType, properties: dict) -> requests.Response:
|
|
139
369
|
resp = request_post(
|
|
140
370
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
|
|
141
|
-
json={
|
|
142
|
-
"action_type": action_type,
|
|
143
|
-
"properties": properties,
|
|
144
|
-
},
|
|
371
|
+
json={"action_type": action_type, "properties": properties},
|
|
145
372
|
headers={
|
|
146
373
|
"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}",
|
|
147
374
|
},
|
|
148
375
|
timeout=REQUESTS_TIMEOUT,
|
|
376
|
+
disable_debug=True,
|
|
377
|
+
)
|
|
378
|
+
resp.raise_for_status()
|
|
379
|
+
return resp
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def finish_upload(
|
|
383
|
+
user_access_token: str,
|
|
384
|
+
file_handle: str,
|
|
385
|
+
cluster_filetype: ClusterFileType,
|
|
386
|
+
organization_id: int | str | None = None,
|
|
387
|
+
) -> requests.Response:
|
|
388
|
+
data: dict[str, str | int] = {
|
|
389
|
+
"file_handle": file_handle,
|
|
390
|
+
"file_type": cluster_filetype.value,
|
|
391
|
+
}
|
|
392
|
+
if organization_id is not None:
|
|
393
|
+
data["organization_id"] = organization_id
|
|
394
|
+
|
|
395
|
+
resp = request_post(
|
|
396
|
+
f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload",
|
|
397
|
+
headers={
|
|
398
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
399
|
+
},
|
|
400
|
+
json=data,
|
|
401
|
+
timeout=REQUESTS_TIMEOUT,
|
|
149
402
|
)
|
|
403
|
+
|
|
150
404
|
resp.raise_for_status()
|
|
405
|
+
|
|
151
406
|
return resp
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def jsonify_response(resp: requests.Response) -> T.Any:
|
|
410
|
+
"""
|
|
411
|
+
Convert the response to JSON, raising HTTPContentError if the response is not JSON.
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
return resp.json()
|
|
415
|
+
except requests.JSONDecodeError as ex:
|
|
416
|
+
raise HTTPContentError("Invalid JSON response", resp) from ex
|