mapillary-tools 0.14.0b1__py3-none-any.whl → 0.14.2__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 (38) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -263
  3. mapillary_tools/authenticate.py +47 -39
  4. mapillary_tools/commands/__main__.py +15 -16
  5. mapillary_tools/commands/upload.py +33 -4
  6. mapillary_tools/config.py +5 -0
  7. mapillary_tools/constants.py +127 -45
  8. mapillary_tools/exceptions.py +4 -0
  9. mapillary_tools/exif_read.py +2 -1
  10. mapillary_tools/exif_write.py +3 -1
  11. mapillary_tools/geo.py +16 -0
  12. mapillary_tools/geotag/base.py +6 -2
  13. mapillary_tools/geotag/factory.py +9 -1
  14. mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
  15. mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
  16. mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
  17. mapillary_tools/geotag/options.py +4 -1
  18. mapillary_tools/geotag/utils.py +9 -12
  19. mapillary_tools/geotag/video_extractors/gpx.py +2 -1
  20. mapillary_tools/geotag/video_extractors/native.py +25 -0
  21. mapillary_tools/history.py +124 -7
  22. mapillary_tools/http.py +211 -0
  23. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  24. mapillary_tools/process_geotag_properties.py +35 -38
  25. mapillary_tools/process_sequence_properties.py +339 -322
  26. mapillary_tools/sample_video.py +1 -2
  27. mapillary_tools/serializer/description.py +68 -58
  28. mapillary_tools/serializer/gpx.py +1 -1
  29. mapillary_tools/upload.py +202 -207
  30. mapillary_tools/upload_api_v4.py +57 -47
  31. mapillary_tools/uploader.py +728 -285
  32. mapillary_tools/utils.py +57 -5
  33. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/METADATA +7 -6
  34. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/RECORD +38 -37
  35. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/WHEEL +0 -0
  36. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/entry_points.txt +0 -0
  37. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/licenses/LICENSE +0 -0
  38. {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/top_level.txt +0 -0
@@ -17,18 +17,11 @@ import tempfile
17
17
 
18
18
  import requests
19
19
 
20
- from .api_v4 import request_get, request_post, REQUESTS_TIMEOUT
20
+ from .api_v4 import HTTPContentError, jsonify_response, REQUESTS_TIMEOUT
21
21
 
22
22
  MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
23
23
  "MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
24
24
  )
25
- # According to the docs, UPLOAD_REQUESTS_TIMEOUT can be a tuple of
26
- # (connection_timeout, read_timeout): https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
27
- # In my test, however, the connection_timeout rules both connection timeout and read timeout.
28
- # i.e. if your the server does not respond within this timeout, it will throw:
29
- # ConnectionError: ('Connection aborted.', timeout('The write operation timed out'))
30
- # So let us make sure the largest possible chunks can be uploaded before this timeout for now,
31
- UPLOAD_REQUESTS_TIMEOUT = (30 * 60, 30 * 60) # 30 minutes
32
25
 
33
26
 
34
27
  class UploadService:
@@ -39,19 +32,21 @@ class UploadService:
39
32
  user_access_token: str
40
33
  session_key: str
41
34
 
42
- def __init__(self, user_access_token: str, session_key: str):
43
- self.user_access_token = user_access_token
35
+ def __init__(self, user_session: requests.Session, session_key: str):
36
+ self.user_session = user_session
44
37
  self.session_key = session_key
45
38
 
46
39
  def fetch_offset(self) -> int:
47
- headers = {
48
- "Authorization": f"OAuth {self.user_access_token}",
49
- }
50
40
  url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
51
- resp = request_get(url, headers=headers, timeout=REQUESTS_TIMEOUT)
41
+
42
+ resp = self.user_session.get(url, timeout=REQUESTS_TIMEOUT)
52
43
  resp.raise_for_status()
53
- data = resp.json()
54
- return data["offset"]
44
+
45
+ data = jsonify_response(resp)
46
+ try:
47
+ return data["offset"]
48
+ except KeyError:
49
+ raise HTTPContentError("Offset not found in the response", resp)
55
50
 
56
51
  @classmethod
57
52
  def chunkize_byte_stream(
@@ -120,47 +115,58 @@ class UploadService:
120
115
  stream: T.IO[bytes],
121
116
  offset: int | None = None,
122
117
  chunk_size: int = 2 * 1024 * 1024, # 2MB
118
+ read_timeout: float | None = None,
123
119
  ) -> str:
124
120
  if offset is None:
125
121
  offset = self.fetch_offset()
126
- return self.upload_chunks(self.chunkize_byte_stream(stream, chunk_size), offset)
122
+ return self.upload_chunks(
123
+ self.chunkize_byte_stream(stream, chunk_size),
124
+ offset,
125
+ read_timeout=read_timeout,
126
+ )
127
127
 
128
128
  def upload_chunks(
129
129
  self,
130
130
  chunks: T.Iterable[bytes],
131
131
  offset: int | None = None,
132
+ read_timeout: float | None = None,
132
133
  ) -> str:
133
134
  if offset is None:
134
135
  offset = self.fetch_offset()
135
136
  shifted_chunks = self.shift_chunks(chunks, offset)
136
- return self.upload_shifted_chunks(shifted_chunks, offset)
137
+ return self.upload_shifted_chunks(
138
+ shifted_chunks, offset, read_timeout=read_timeout
139
+ )
137
140
 
138
141
  def upload_shifted_chunks(
139
- self, shifted_chunks: T.Iterable[bytes], offset: int
142
+ self,
143
+ shifted_chunks: T.Iterable[bytes],
144
+ offset: int,
145
+ read_timeout: float | None = None,
140
146
  ) -> str:
141
147
  """
142
148
  Upload the chunks that must already be shifted by the offset (e.g. fp.seek(offset, io.SEEK_SET))
143
149
  """
144
150
 
151
+ url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
145
152
  headers = {
146
- "Authorization": f"OAuth {self.user_access_token}",
147
153
  "Offset": f"{offset}",
148
154
  "X-Entity-Name": self.session_key,
149
155
  }
150
- url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
151
- resp = request_post(
152
- url, headers=headers, data=shifted_chunks, timeout=UPLOAD_REQUESTS_TIMEOUT
153
- )
154
156
 
157
+ resp = self.user_session.post(
158
+ url,
159
+ headers=headers,
160
+ data=shifted_chunks,
161
+ timeout=(REQUESTS_TIMEOUT, read_timeout), # type: ignore
162
+ )
155
163
  resp.raise_for_status()
156
164
 
157
- payload = resp.json()
165
+ data = jsonify_response(resp)
158
166
  try:
159
- return payload["h"]
167
+ return data["h"]
160
168
  except KeyError:
161
- raise RuntimeError(
162
- f"Upload server error: File handle not found in the upload response {resp.text}"
163
- )
169
+ raise HTTPContentError("File handle not found in the response", resp)
164
170
 
165
171
 
166
172
  # A mock class for testing only
@@ -174,9 +180,9 @@ class FakeUploadService(UploadService):
174
180
 
175
181
  def __init__(
176
182
  self,
183
+ *args,
177
184
  upload_path: Path | None = None,
178
185
  transient_error_ratio: float = 0.0,
179
- *args,
180
186
  **kwargs,
181
187
  ):
182
188
  super().__init__(*args, **kwargs)
@@ -187,13 +193,12 @@ class FakeUploadService(UploadService):
187
193
  self._upload_path = upload_path
188
194
  self._transient_error_ratio = transient_error_ratio
189
195
 
190
- @property
191
- def upload_path(self) -> Path:
192
- return self._upload_path
193
-
194
196
  @override
195
197
  def upload_shifted_chunks(
196
- self, shifted_chunks: T.Iterable[bytes], offset: int
198
+ self,
199
+ shifted_chunks: T.Iterable[bytes],
200
+ offset: int,
201
+ read_timeout: float | None = None,
197
202
  ) -> str:
198
203
  expected_offset = self.fetch_offset()
199
204
  if offset != expected_offset:
@@ -205,15 +210,9 @@ class FakeUploadService(UploadService):
205
210
  filename = self._upload_path.joinpath(self.session_key)
206
211
  with filename.open("ab") as fp:
207
212
  for chunk in shifted_chunks:
208
- if random.random() <= self._transient_error_ratio:
209
- raise requests.ConnectionError(
210
- f"TEST ONLY: Failed to upload with error ratio {self._transient_error_ratio}"
211
- )
213
+ self._randomly_raise_transient_error()
212
214
  fp.write(chunk)
213
- if random.random() <= self._transient_error_ratio:
214
- raise requests.ConnectionError(
215
- f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
216
- )
215
+ self._randomly_raise_transient_error()
217
216
 
218
217
  file_handle_dir = self._upload_path.joinpath(self.FILE_HANDLE_DIR)
219
218
  file_handle_path = file_handle_dir.joinpath(self.session_key)
@@ -226,13 +225,24 @@ class FakeUploadService(UploadService):
226
225
 
227
226
  @override
228
227
  def fetch_offset(self) -> int:
229
- if random.random() <= self._transient_error_ratio:
230
- raise requests.ConnectionError(
231
- f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
232
- )
228
+ self._randomly_raise_transient_error()
233
229
  filename = self._upload_path.joinpath(self.session_key)
234
230
  if not filename.exists():
235
231
  return 0
236
232
  with open(filename, "rb") as fp:
237
233
  fp.seek(0, io.SEEK_END)
238
234
  return fp.tell()
235
+
236
+ @property
237
+ def upload_path(self) -> Path:
238
+ return self._upload_path
239
+
240
+ def _randomly_raise_transient_error(self):
241
+ """
242
+ Randomly raise a transient error based on the configured error ratio.
243
+ This is for testing purposes only.
244
+ """
245
+ if random.random() <= self._transient_error_ratio:
246
+ raise requests.ConnectionError(
247
+ f"[TEST ONLY]: Transient error with ratio {self._transient_error_ratio}"
248
+ )