mapillary-tools 0.13.2__tar.gz → 0.13.3__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.
Files changed (84) hide show
  1. {mapillary_tools-0.13.2/mapillary_tools.egg-info → mapillary_tools-0.13.3}/PKG-INFO +1 -1
  2. mapillary_tools-0.13.3/mapillary_tools/__init__.py +1 -0
  3. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/api_v4.py +133 -11
  4. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/__main__.py +9 -4
  5. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/process_and_upload.py +1 -0
  6. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/constants.py +1 -1
  7. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/upload.py +11 -43
  8. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/upload_api_v4.py +72 -92
  9. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/uploader.py +0 -2
  10. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3/mapillary_tools.egg-info}/PKG-INFO +1 -1
  11. mapillary_tools-0.13.2/mapillary_tools/__init__.py +0 -1
  12. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/LICENSE +0 -0
  13. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/MANIFEST.in +0 -0
  14. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/README.md +0 -0
  15. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/authenticate.py +0 -0
  16. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/camm/camm_builder.py +0 -0
  17. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/camm/camm_parser.py +0 -0
  18. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/__init__.py +0 -0
  19. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/authenticate.py +0 -0
  20. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/process.py +0 -0
  21. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/sample_video.py +0 -0
  22. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/upload.py +0 -0
  23. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/video_process.py +0 -0
  24. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/video_process_and_upload.py +0 -0
  25. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/commands/zip.py +0 -0
  26. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/config.py +0 -0
  27. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/exceptions.py +0 -0
  28. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/exif_read.py +0 -0
  29. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/exif_write.py +0 -0
  30. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/exiftool_read.py +0 -0
  31. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/exiftool_read_video.py +0 -0
  32. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/ffmpeg.py +0 -0
  33. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geo.py +0 -0
  34. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/__init__.py +0 -0
  35. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/blackvue_parser.py +0 -0
  36. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_from_generic.py +0 -0
  37. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_exif.py +0 -0
  38. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -0
  39. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -0
  40. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -0
  41. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -0
  42. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
  43. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
  44. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -0
  45. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/geotag_videos_from_video.py +0 -0
  46. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/gpmf_gps_filter.py +0 -0
  47. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/gpmf_parser.py +0 -0
  48. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/gps_filter.py +0 -0
  49. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/geotag/utils.py +0 -0
  50. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/history.py +0 -0
  51. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/ipc.py +0 -0
  52. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/mp4/__init__.py +0 -0
  53. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/mp4/construct_mp4_parser.py +0 -0
  54. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/mp4/io_utils.py +0 -0
  55. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/mp4/mp4_sample_parser.py +0 -0
  56. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
  57. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/mp4/simple_mp4_parser.py +0 -0
  58. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/process_geotag_properties.py +0 -0
  59. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/process_sequence_properties.py +0 -0
  60. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/sample_video.py +0 -0
  61. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/telemetry.py +0 -0
  62. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/types.py +0 -0
  63. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/utils.py +0 -0
  64. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/cli_options.py +0 -0
  65. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extract_video_data.py +0 -0
  66. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -0
  67. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -0
  68. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -0
  69. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -0
  70. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -0
  71. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -0
  72. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -0
  73. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -0
  74. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -0
  75. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -0
  76. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools.egg-info/SOURCES.txt +0 -0
  77. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools.egg-info/dependency_links.txt +0 -0
  78. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools.egg-info/entry_points.txt +0 -0
  79. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools.egg-info/requires.txt +0 -0
  80. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/mapillary_tools.egg-info/top_level.txt +0 -0
  81. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/requirements.txt +0 -0
  82. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/schema/image_description_schema.json +0 -0
  83. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/setup.cfg +0 -0
  84. {mapillary_tools-0.13.2 → mapillary_tools-0.13.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mapillary_tools
3
- Version: 0.13.2
3
+ Version: 0.13.3
4
4
  Summary: Mapillary Image/Video Import Pipeline
5
5
  Home-page: https://github.com/mapillary/mapillary_tools
6
6
  Author: Mapillary
@@ -0,0 +1 @@
1
+ VERSION = "0.13.3"
@@ -2,6 +2,7 @@ import logging
2
2
  import os
3
3
  import ssl
4
4
  import typing as T
5
+ from json import dumps
5
6
 
6
7
  import requests
7
8
  from requests.adapters import HTTPAdapter
@@ -46,6 +47,106 @@ class HTTPSystemCertsAdapter(HTTPAdapter):
46
47
  conn.ca_certs = None
47
48
 
48
49
 
50
+ @T.overload
51
+ def _truncate(s: bytes, limit: int = 512) -> bytes: ...
52
+
53
+
54
+ @T.overload
55
+ def _truncate(s: str, limit: int = 512) -> str: ...
56
+
57
+
58
+ def _truncate(s, limit=512):
59
+ if limit < len(s):
60
+ remaining = len(s) - limit
61
+ if isinstance(s, bytes):
62
+ return (
63
+ s[:limit]
64
+ + b"..."
65
+ + f"({remaining} more bytes truncated)".encode("utf-8")
66
+ )
67
+ else:
68
+ return str(s[:limit]) + f"...({remaining} more chars truncated)"
69
+ else:
70
+ return s
71
+
72
+
73
+ def _sanitize(headers: T.Dict):
74
+ new_headers = {}
75
+
76
+ for k, v in headers.items():
77
+ if k.lower() in [
78
+ "authorization",
79
+ "cookie",
80
+ "x-fb-access-token",
81
+ "access-token",
82
+ "access_token",
83
+ "password",
84
+ ]:
85
+ new_headers[k] = "[REDACTED]"
86
+ else:
87
+ new_headers[k] = _truncate(v)
88
+
89
+ return new_headers
90
+
91
+
92
+ def _log_debug_request(
93
+ method: str,
94
+ url: str,
95
+ json: T.Optional[T.Dict] = None,
96
+ params: T.Optional[T.Dict] = None,
97
+ headers: T.Optional[T.Dict] = None,
98
+ timeout: T.Any = None,
99
+ ):
100
+ if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
101
+ return
102
+
103
+ msg = f"HTTP {method} {url}"
104
+
105
+ if USE_SYSTEM_CERTS:
106
+ msg += " (w/sys_certs)"
107
+
108
+ if json:
109
+ t = _truncate(dumps(_sanitize(json)))
110
+ msg += f" JSON={t}"
111
+
112
+ if params:
113
+ msg += f" PARAMS={_sanitize(params)}"
114
+
115
+ if headers:
116
+ msg += f" HEADERS={_sanitize(headers)}"
117
+
118
+ if timeout is not None:
119
+ msg += f" TIMEOUT={timeout}"
120
+
121
+ LOG.debug(msg)
122
+
123
+
124
+ def _log_debug_response(resp: requests.Response):
125
+ if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
126
+ return
127
+
128
+ data: T.Union[str, bytes]
129
+ try:
130
+ data = _truncate(dumps(_sanitize(resp.json())))
131
+ except Exception:
132
+ data = _truncate(resp.content)
133
+
134
+ LOG.debug(f"HTTP {resp.status_code} ({resp.reason}): %s", data)
135
+
136
+
137
+ def readable_http_error(ex: requests.HTTPError) -> str:
138
+ req = ex.request
139
+ resp = ex.response
140
+
141
+ data: T.Union[str, bytes]
142
+ try:
143
+ data = _truncate(dumps(_sanitize(resp.json())))
144
+ except Exception:
145
+ data = _truncate(resp.content)
146
+
147
+ return f"{req.method} {resp.url} => {resp.status_code} ({resp.reason}): {str(data)}"
148
+
149
+
49
150
  def request_post(
50
151
  url: str,
51
152
  data: T.Optional[T.Any] = None,
@@ -54,14 +155,23 @@ def request_post(
54
155
  ) -> requests.Response:
55
156
  global USE_SYSTEM_CERTS
56
157
 
158
+ _log_debug_request(
159
+ "POST",
160
+ url,
161
+ json=json,
162
+ params=kwargs.get("params"),
163
+ headers=kwargs.get("headers"),
164
+ timeout=kwargs.get("timeout"),
165
+ )
166
+
57
167
  if USE_SYSTEM_CERTS:
58
168
  with requests.Session() as session:
59
169
  session.mount("https://", HTTPSystemCertsAdapter())
60
- return session.post(url, data=data, json=json, **kwargs)
170
+ resp = session.post(url, data=data, json=json, **kwargs)
61
171
 
62
172
  else:
63
173
  try:
64
- return requests.post(url, data=data, json=json, **kwargs)
174
+ resp = requests.post(url, data=data, json=json, **kwargs)
65
175
  except requests.exceptions.SSLError as ex:
66
176
  if "SSLCertVerificationError" not in str(ex):
67
177
  raise ex
@@ -70,9 +180,11 @@ def request_post(
70
180
  LOG.warning(
71
181
  "SSL error occurred, falling back to system SSL certificates: %s", ex
72
182
  )
73
- with requests.Session() as session:
74
- session.mount("https://", HTTPSystemCertsAdapter())
75
- return session.post(url, data=data, json=json, **kwargs)
183
+ return request_post(url, data=data, json=json, **kwargs)
184
+
185
+ _log_debug_response(resp)
186
+
187
+ return resp
76
188
 
77
189
 
78
190
  def request_get(
@@ -82,13 +194,21 @@ def request_get(
82
194
  ) -> requests.Response:
83
195
  global USE_SYSTEM_CERTS
84
196
 
197
+ _log_debug_request(
198
+ "GET",
199
+ url,
200
+ params=kwargs.get("params"),
201
+ headers=kwargs.get("headers"),
202
+ timeout=kwargs.get("timeout"),
203
+ )
204
+
85
205
  if USE_SYSTEM_CERTS:
86
206
  with requests.Session() as session:
87
207
  session.mount("https://", HTTPSystemCertsAdapter())
88
- return session.get(url, params=params, **kwargs)
208
+ resp = session.get(url, params=params, **kwargs)
89
209
  else:
90
210
  try:
91
- return requests.get(url, params=params, **kwargs)
211
+ resp = requests.get(url, params=params, **kwargs)
92
212
  except requests.exceptions.SSLError as ex:
93
213
  if "SSLCertVerificationError" not in str(ex):
94
214
  raise ex
@@ -97,15 +217,17 @@ def request_get(
97
217
  LOG.warning(
98
218
  "SSL error occurred, falling back to system SSL certificates: %s", ex
99
219
  )
100
- with requests.Session() as session:
101
- session.mount("https://", HTTPSystemCertsAdapter())
102
- return session.get(url, params=params, **kwargs)
220
+ resp = request_get(url, params=params, **kwargs)
221
+
222
+ _log_debug_response(resp)
223
+
224
+ return resp
103
225
 
104
226
 
105
227
  def get_upload_token(email: str, password: str) -> requests.Response:
106
228
  resp = request_post(
107
229
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
108
- params={"access_token": MAPILLARY_CLIENT_TOKEN},
230
+ headers={"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}"},
109
231
  json={"email": email, "password": password, "locale": "en_US"},
110
232
  timeout=REQUESTS_TIMEOUT,
111
233
  )
@@ -5,7 +5,9 @@ import sys
5
5
  import typing as T
6
6
  from pathlib import Path
7
7
 
8
- from .. import constants, exceptions, VERSION
8
+ import requests
9
+
10
+ from .. import api_v4, constants, exceptions, VERSION
9
11
  from . import (
10
12
  authenticate,
11
13
  process,
@@ -160,11 +162,14 @@ def main():
160
162
 
161
163
  try:
162
164
  args.func(argvars)
163
- except exceptions.MapillaryUserError as exc:
165
+ except requests.HTTPError as ex:
166
+ LOG.error("%s: %s", ex.__class__.__name__, api_v4.readable_http_error(ex))
167
+
168
+ except exceptions.MapillaryUserError as ex:
164
169
  LOG.error(
165
- "%s: %s", exc.__class__.__name__, exc, exc_info=log_level == logging.DEBUG
170
+ "%s: %s", ex.__class__.__name__, ex, exc_info=log_level == logging.DEBUG
166
171
  )
167
- sys.exit(exc.exit_code)
172
+ sys.exit(ex.exit_code)
168
173
 
169
174
 
170
175
  if __name__ == "__main__":
@@ -15,5 +15,6 @@ class Command:
15
15
  # \x00 is a special path similiar to /dev/null
16
16
  # it tells process command do not write anything
17
17
  args["desc_path"] = "\x00"
18
+
18
19
  ProcessCommand().run(args)
19
20
  UploadCommand().run(args)
@@ -45,6 +45,6 @@ GOPRO_GPS_PRECISION = float(os.getenv(_ENV_PREFIX + "GOPRO_GPS_PRECISION", 15))
45
45
  # Max number of images per sequence
46
46
  MAX_SEQUENCE_LENGTH = int(os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_LENGTH", 1000))
47
47
  # Max file size per sequence (sum of image filesizes in the sequence)
48
- MAX_SEQUENCE_FILESIZE: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_FILESIZE", "10G")
48
+ MAX_SEQUENCE_FILESIZE: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_FILESIZE", "110G")
49
49
  # Max number of pixels per sequence (sum of image pixels in the sequence)
50
50
  MAX_SEQUENCE_PIXELS: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_PIXELS", "6G")
@@ -47,25 +47,6 @@ class UploadError(Exception):
47
47
  super().__init__(str(inner_ex))
48
48
 
49
49
 
50
- class UploadHTTPError(Exception):
51
- pass
52
-
53
-
54
- def wrap_http_exception(ex: requests.HTTPError):
55
- req = ex.request
56
- resp = ex.response
57
- if isinstance(resp, requests.Response) and isinstance(req, requests.Request):
58
- lines = [
59
- f"{req.method} {resp.url}",
60
- f"> HTTP Status: {resp.status_code}",
61
- str(resp.content),
62
- ]
63
- else:
64
- lines = []
65
-
66
- return UploadHTTPError("\n".join(lines))
67
-
68
-
69
50
  def _load_validate_metadatas_from_desc_path(
70
51
  desc_path: T.Optional[str], import_paths: T.Sequence[Path]
71
52
  ) -> T.List[types.Metadata]:
@@ -175,18 +156,12 @@ def fetch_user_items(
175
156
  "Found multiple Mapillary accounts. Please specify one with --user_name"
176
157
  )
177
158
  else:
178
- try:
179
- user_items = authenticate.authenticate_user(user_name)
180
- except requests.HTTPError as exc:
181
- raise wrap_http_exception(exc) from exc
159
+ user_items = authenticate.authenticate_user(user_name)
182
160
 
183
161
  if organization_key is not None:
184
- try:
185
- resp = api_v4.fetch_organization(
186
- user_items["user_upload_token"], organization_key
187
- )
188
- except requests.HTTPError as ex:
189
- raise wrap_http_exception(ex) from ex
162
+ resp = api_v4.fetch_organization(
163
+ user_items["user_upload_token"], organization_key
164
+ )
190
165
  org = resp.json()
191
166
  LOG.info("Uploading to organization: %s", json.dumps(org))
192
167
  user_items = T.cast(
@@ -430,15 +405,12 @@ def _api_logging_finished(summary: T.Dict):
430
405
  action: api_v4.ActionType = "upload_finished_upload"
431
406
  LOG.debug("API Logging for action %s: %s", action, summary)
432
407
  try:
433
- api_v4.log_event(
434
- action,
435
- summary,
436
- )
408
+ api_v4.log_event(action, summary)
437
409
  except requests.HTTPError as exc:
438
410
  LOG.warning(
439
- "Error from API Logging for action %s",
411
+ "HTTPError from API Logging for action %s: %s",
440
412
  action,
441
- exc_info=wrap_http_exception(exc),
413
+ api_v4.readable_http_error(exc),
442
414
  )
443
415
  except Exception:
444
416
  LOG.warning("Error from API Logging for action %s", action, exc_info=True)
@@ -452,16 +424,12 @@ def _api_logging_failed(payload: T.Dict, exc: Exception):
452
424
  action: api_v4.ActionType = "upload_failed_upload"
453
425
  LOG.debug("API Logging for action %s: %s", action, payload)
454
426
  try:
455
- api_v4.log_event(
456
- action,
457
- payload_with_reason,
458
- )
427
+ api_v4.log_event(action, payload_with_reason)
459
428
  except requests.HTTPError as exc:
460
- wrapped_exc = wrap_http_exception(exc)
461
429
  LOG.warning(
462
- "Error from API Logging for action %s",
430
+ "HTTPError from API Logging for action %s: %s",
463
431
  action,
464
- exc_info=wrapped_exc,
432
+ api_v4.readable_http_error(exc),
465
433
  )
466
434
  except Exception:
467
435
  LOG.warning("Error from API Logging for action %s", action, exc_info=True)
@@ -678,7 +646,7 @@ def upload(
678
646
  raise exceptions.MapillaryUploadUnauthorizedError(
679
647
  debug_info.get("message")
680
648
  ) from inner_ex
681
- raise wrap_http_exception(inner_ex) from inner_ex
649
+ raise inner_ex
682
650
 
683
651
  raise inner_ex
684
652
 
@@ -1,16 +1,19 @@
1
1
  import enum
2
2
  import io
3
- import json
4
- import logging
5
3
  import os
6
4
  import random
7
5
  import typing as T
6
+ import uuid
8
7
 
9
8
  import requests
10
9
 
11
- from .api_v4 import MAPILLARY_GRAPH_API_ENDPOINT, request_get, request_post
10
+ from .api_v4 import (
11
+ MAPILLARY_GRAPH_API_ENDPOINT,
12
+ request_get,
13
+ request_post,
14
+ REQUESTS_TIMEOUT,
15
+ )
12
16
 
13
- LOG = logging.getLogger(__name__)
14
17
  MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
15
18
  "MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
16
19
  )
@@ -21,7 +24,6 @@ DEFAULT_CHUNK_SIZE = 1024 * 1024 * 16 # 16MB
21
24
  # i.e. if your the server does not respond within this timeout, it will throw:
22
25
  # ConnectionError: ('Connection aborted.', timeout('The write operation timed out'))
23
26
  # So let us make sure the largest possible chunks can be uploaded before this timeout for now,
24
- REQUESTS_TIMEOUT = (20, 20) # 20 seconds
25
27
  UPLOAD_REQUESTS_TIMEOUT = (30 * 60, 30 * 60) # 30 minutes
26
28
 
27
29
 
@@ -31,55 +33,33 @@ class ClusterFileType(enum.Enum):
31
33
  CAMM = "mly_camm_video"
32
34
 
33
35
 
34
- def _sanitize_headers(headers: T.Dict):
35
- return {
36
- k: v
37
- for k, v in headers.items()
38
- if k.lower() not in ["authorization", "cookie", "x-fb-access-token"]
39
- }
40
-
41
-
42
- _S = T.TypeVar("_S", str, bytes)
43
-
44
-
45
- def _truncate_end(s: _S) -> _S:
46
- MAX_LENGTH = 512
47
- if MAX_LENGTH < len(s):
48
- if isinstance(s, bytes):
49
- return s[:MAX_LENGTH] + b"..."
50
- else:
51
- return str(s[:MAX_LENGTH]) + "..."
52
- else:
53
- return s
54
-
55
-
56
36
  class UploadService:
57
37
  user_access_token: str
58
- entity_size: int
59
38
  session_key: str
60
39
  callbacks: T.List[T.Callable[[bytes, T.Optional[requests.Response]], None]]
61
40
  cluster_filetype: ClusterFileType
62
41
  organization_id: T.Optional[T.Union[str, int]]
63
42
  chunk_size: int
64
43
 
44
+ MIME_BY_CLUSTER_TYPE: T.Dict[ClusterFileType, str] = {
45
+ ClusterFileType.ZIP: "application/zip",
46
+ ClusterFileType.BLACKVUE: "video/mp4",
47
+ ClusterFileType.CAMM: "video/mp4",
48
+ }
49
+
65
50
  def __init__(
66
51
  self,
67
52
  user_access_token: str,
68
53
  session_key: str,
69
- entity_size: int,
70
54
  organization_id: T.Optional[T.Union[str, int]] = None,
71
55
  cluster_filetype: ClusterFileType = ClusterFileType.ZIP,
72
56
  chunk_size: int = DEFAULT_CHUNK_SIZE,
73
57
  ):
74
- if entity_size <= 0:
75
- raise ValueError(f"Expect positive entity size but got {entity_size}")
76
-
77
58
  if chunk_size <= 0:
78
59
  raise ValueError("Expect positive chunk size")
79
60
 
80
61
  self.user_access_token = user_access_token
81
62
  self.session_key = session_key
82
- self.entity_size = entity_size
83
63
  self.organization_id = organization_id
84
64
  # validate the input
85
65
  self.cluster_filetype = ClusterFileType(cluster_filetype)
@@ -91,13 +71,11 @@ class UploadService:
91
71
  "Authorization": f"OAuth {self.user_access_token}",
92
72
  }
93
73
  url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
94
- LOG.debug("GET %s", url)
95
74
  resp = request_get(
96
75
  url,
97
76
  headers=headers,
98
77
  timeout=REQUESTS_TIMEOUT,
99
78
  )
100
- LOG.debug("HTTP response %s: %s", resp.status_code, resp.content)
101
79
  resp.raise_for_status()
102
80
  data = resp.json()
103
81
  return data["offset"]
@@ -107,54 +85,63 @@ class UploadService:
107
85
  data: T.IO[bytes],
108
86
  offset: T.Optional[int] = None,
109
87
  ) -> str:
110
- if offset is None:
111
- offset = self.fetch_offset()
112
-
113
- entity_type_map: T.Dict[ClusterFileType, str] = {
114
- ClusterFileType.ZIP: "application/zip",
115
- ClusterFileType.BLACKVUE: "video/mp4",
116
- ClusterFileType.CAMM: "video/mp4",
117
- }
118
-
119
- entity_type = entity_type_map[self.cluster_filetype]
120
-
121
- data.seek(offset, io.SEEK_CUR)
88
+ chunks = self._chunkize_byte_stream(data)
89
+ return self.upload_chunks(chunks, offset=offset)
122
90
 
91
+ def _chunkize_byte_stream(
92
+ self, stream: T.IO[bytes]
93
+ ) -> T.Generator[bytes, None, None]:
123
94
  while True:
124
- chunk = data.read(self.chunk_size)
125
- # it is possible to upload an empty chunk here
126
- # in order to return the handle
127
- headers = {
128
- "Authorization": f"OAuth {self.user_access_token}",
129
- "Offset": f"{offset}",
130
- "X-Entity-Length": str(self.entity_size),
131
- "X-Entity-Name": self.session_key,
132
- "X-Entity-Type": entity_type,
133
- }
134
- url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
135
- LOG.debug("POST %s HEADERS %s", url, json.dumps(_sanitize_headers(headers)))
136
- resp = request_post(
137
- url,
138
- headers=headers,
139
- data=chunk,
140
- timeout=UPLOAD_REQUESTS_TIMEOUT,
141
- )
142
- LOG.debug(
143
- "HTTP response %s: %s", resp.status_code, _truncate_end(resp.content)
144
- )
145
- resp.raise_for_status()
146
- offset += len(chunk)
147
- LOG.debug("The next offset will be: %s", offset)
95
+ data = stream.read(self.chunk_size)
96
+ if not data:
97
+ break
98
+ yield data
99
+
100
+ def _offset_chunks(
101
+ self, chunks: T.Iterable[bytes], offset: int
102
+ ) -> T.Generator[bytes, None, None]:
103
+ assert offset >= 0, f"Expect non-negative offset but got {offset}"
104
+
105
+ for chunk in chunks:
106
+ if offset:
107
+ if offset < len(chunk):
108
+ yield chunk[offset:]
109
+ offset = 0
110
+ else:
111
+ offset -= len(chunk)
112
+ else:
113
+ yield chunk
114
+
115
+ def _attach_callbacks(
116
+ self, chunks: T.Iterable[bytes]
117
+ ) -> T.Generator[bytes, None, None]:
118
+ for chunk in chunks:
119
+ yield chunk
148
120
  for callback in self.callbacks:
149
- callback(chunk, resp)
150
- # we can assert that offset == self.fetch_offset(session_key)
151
- # otherwise, server will throw
121
+ callback(chunk, None)
152
122
 
153
- if not chunk:
154
- break
123
+ def upload_chunks(
124
+ self,
125
+ chunks: T.Iterable[bytes],
126
+ offset: T.Optional[int] = None,
127
+ ) -> str:
128
+ if offset is None:
129
+ offset = self.fetch_offset()
130
+
131
+ chunks = self._attach_callbacks(self._offset_chunks(chunks, offset))
155
132
 
156
- assert offset == self.entity_size, (
157
- f"Offset ends at {offset} but the entity size is {self.entity_size}"
133
+ headers = {
134
+ "Authorization": f"OAuth {self.user_access_token}",
135
+ "Offset": f"{offset}",
136
+ "X-Entity-Name": self.session_key,
137
+ "X-Entity-Type": self.MIME_BY_CLUSTER_TYPE[self.cluster_filetype],
138
+ }
139
+ url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
140
+ resp = request_post(
141
+ url,
142
+ headers=headers,
143
+ data=chunks,
144
+ timeout=UPLOAD_REQUESTS_TIMEOUT,
158
145
  )
159
146
 
160
147
  payload = resp.json()
@@ -178,14 +165,12 @@ class UploadService:
178
165
 
179
166
  url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
180
167
 
181
- LOG.debug("POST %s HEADERS %s", url, json.dumps(_sanitize_headers(headers)))
182
168
  resp = request_post(
183
169
  url,
184
170
  headers=headers,
185
171
  json=data,
186
172
  timeout=REQUESTS_TIMEOUT,
187
173
  )
188
- LOG.debug("HTTP response %s: %s", resp.status_code, _truncate_end(resp.content))
189
174
 
190
175
  resp.raise_for_status()
191
176
 
@@ -209,35 +194,30 @@ class FakeUploadService(UploadService):
209
194
  )
210
195
  self._error_ratio = 0.1
211
196
 
212
- def upload(
197
+ def upload_chunks(
213
198
  self,
214
- data: T.IO[bytes],
199
+ chunks: T.Iterable[bytes],
215
200
  offset: T.Optional[int] = None,
216
201
  ) -> str:
217
202
  if offset is None:
218
203
  offset = self.fetch_offset()
204
+
205
+ chunks = self._attach_callbacks(self._offset_chunks(chunks, offset))
206
+
219
207
  os.makedirs(self._upload_path, exist_ok=True)
220
208
  filename = os.path.join(self._upload_path, self.session_key)
221
209
  with open(filename, "ab") as fp:
222
- data.seek(offset, io.SEEK_CUR)
223
- while True:
224
- chunk = data.read(self.chunk_size)
225
- if not chunk:
226
- break
227
- # fail here means nothing uploaded
210
+ for chunk in chunks:
228
211
  if random.random() <= self._error_ratio:
229
212
  raise requests.ConnectionError(
230
213
  f"TEST ONLY: Failed to upload with error ratio {self._error_ratio}"
231
214
  )
232
215
  fp.write(chunk)
233
- # fail here means patially uploaded
234
216
  if random.random() <= self._error_ratio:
235
217
  raise requests.ConnectionError(
236
218
  f"TEST ONLY: Partially uploaded with error ratio {self._error_ratio}"
237
219
  )
238
- for callback in self.callbacks:
239
- callback(chunk, None)
240
- return self.session_key
220
+ return uuid.uuid4().hex
241
221
 
242
222
  def finish(self, _: str) -> str:
243
223
  return "0"
@@ -195,7 +195,6 @@ class Uploader:
195
195
  upload_api_v4.FakeUploadService(
196
196
  user_access_token=self.user_items["user_upload_token"],
197
197
  session_key=session_key,
198
- entity_size=entity_size,
199
198
  organization_id=self.user_items.get("MAPOrganizationKey"),
200
199
  cluster_filetype=cluster_filetype,
201
200
  chunk_size=self.chunk_size,
@@ -205,7 +204,6 @@ class Uploader:
205
204
  upload_service = upload_api_v4.UploadService(
206
205
  user_access_token=self.user_items["user_upload_token"],
207
206
  session_key=session_key,
208
- entity_size=entity_size,
209
207
  organization_id=self.user_items.get("MAPOrganizationKey"),
210
208
  cluster_filetype=cluster_filetype,
211
209
  chunk_size=self.chunk_size,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mapillary_tools
3
- Version: 0.13.2
3
+ Version: 0.13.3
4
4
  Summary: Mapillary Image/Video Import Pipeline
5
5
  Home-page: https://github.com/mapillary/mapillary_tools
6
6
  Author: Mapillary
@@ -1 +0,0 @@
1
- VERSION = "0.13.2"