mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.0b1__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 +1 -0
  3. mapillary_tools/authenticate.py +9 -9
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/config.py +38 -17
  6. mapillary_tools/constants.py +2 -0
  7. mapillary_tools/exiftool_read_video.py +52 -15
  8. mapillary_tools/exiftool_runner.py +4 -24
  9. mapillary_tools/ffmpeg.py +406 -232
  10. mapillary_tools/geotag/__init__.py +0 -0
  11. mapillary_tools/geotag/base.py +2 -2
  12. mapillary_tools/geotag/factory.py +97 -88
  13. mapillary_tools/geotag/geotag_images_from_exiftool.py +26 -19
  14. mapillary_tools/geotag/geotag_images_from_gpx.py +13 -6
  15. mapillary_tools/geotag/geotag_images_from_video.py +35 -0
  16. mapillary_tools/geotag/geotag_videos_from_exiftool.py +39 -13
  17. mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
  18. mapillary_tools/geotag/options.py +25 -3
  19. mapillary_tools/geotag/video_extractors/base.py +1 -1
  20. mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
  21. mapillary_tools/geotag/video_extractors/gpx.py +60 -70
  22. mapillary_tools/geotag/video_extractors/native.py +9 -31
  23. mapillary_tools/history.py +4 -1
  24. mapillary_tools/process_geotag_properties.py +16 -8
  25. mapillary_tools/process_sequence_properties.py +9 -11
  26. mapillary_tools/sample_video.py +7 -6
  27. mapillary_tools/serializer/description.py +587 -0
  28. mapillary_tools/serializer/gpx.py +132 -0
  29. mapillary_tools/types.py +44 -610
  30. mapillary_tools/upload.py +176 -197
  31. mapillary_tools/upload_api_v4.py +94 -51
  32. mapillary_tools/uploader.py +284 -138
  33. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
  34. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/RECORD +38 -35
  35. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
  36. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
  37. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
  38. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
@@ -6,15 +6,18 @@ import random
6
6
  import sys
7
7
  import typing as T
8
8
  import uuid
9
+ from pathlib import Path
9
10
 
10
11
  if sys.version_info >= (3, 12):
11
12
  from typing import override
12
13
  else:
13
14
  from typing_extensions import override
14
15
 
16
+ import tempfile
17
+
15
18
  import requests
16
19
 
17
- from .api_v4 import ClusterFileType, request_get, request_post, REQUESTS_TIMEOUT
20
+ from .api_v4 import request_get, request_post, REQUESTS_TIMEOUT
18
21
 
19
22
  MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
20
23
  "MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
@@ -29,37 +32,23 @@ UPLOAD_REQUESTS_TIMEOUT = (30 * 60, 30 * 60) # 30 minutes
29
32
 
30
33
 
31
34
  class UploadService:
35
+ """
36
+ Upload byte streams to the Upload Service.
37
+ """
38
+
32
39
  user_access_token: str
33
40
  session_key: str
34
- cluster_filetype: ClusterFileType
35
-
36
- MIME_BY_CLUSTER_TYPE: dict[ClusterFileType, str] = {
37
- ClusterFileType.ZIP: "application/zip",
38
- ClusterFileType.BLACKVUE: "video/mp4",
39
- ClusterFileType.CAMM: "video/mp4",
40
- }
41
41
 
42
- def __init__(
43
- self,
44
- user_access_token: str,
45
- session_key: str,
46
- cluster_filetype: ClusterFileType,
47
- ):
42
+ def __init__(self, user_access_token: str, session_key: str):
48
43
  self.user_access_token = user_access_token
49
44
  self.session_key = session_key
50
- # Validate the input
51
- self.cluster_filetype = cluster_filetype
52
45
 
53
46
  def fetch_offset(self) -> int:
54
47
  headers = {
55
48
  "Authorization": f"OAuth {self.user_access_token}",
56
49
  }
57
50
  url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
58
- resp = request_get(
59
- url,
60
- headers=headers,
61
- timeout=REQUESTS_TIMEOUT,
62
- )
51
+ resp = request_get(url, headers=headers, timeout=REQUESTS_TIMEOUT)
63
52
  resp.raise_for_status()
64
53
  data = resp.json()
65
54
  return data["offset"]
@@ -68,18 +57,53 @@ class UploadService:
68
57
  def chunkize_byte_stream(
69
58
  cls, stream: T.IO[bytes], chunk_size: int
70
59
  ) -> T.Generator[bytes, None, None]:
60
+ """
61
+ Chunkize a byte stream into chunks of the specified size.
62
+
63
+ >>> list(UploadService.chunkize_byte_stream(io.BytesIO(b"foo"), 1))
64
+ [b'f', b'o', b'o']
65
+
66
+ >>> list(UploadService.chunkize_byte_stream(io.BytesIO(b"foo"), 10))
67
+ [b'foo']
68
+ """
69
+
71
70
  if chunk_size <= 0:
72
71
  raise ValueError("Expect positive chunk size")
72
+
73
73
  while True:
74
74
  data = stream.read(chunk_size)
75
75
  if not data:
76
76
  break
77
77
  yield data
78
78
 
79
+ @classmethod
79
80
  def shift_chunks(
80
- self, chunks: T.Iterable[bytes], offset: int
81
+ cls, chunks: T.Iterable[bytes], offset: int
81
82
  ) -> T.Generator[bytes, None, None]:
82
- assert offset >= 0, f"Expect non-negative offset but got {offset}"
83
+ """
84
+ Shift the chunks by the offset.
85
+
86
+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 0))
87
+ [b'foo', b'bar']
88
+
89
+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 1))
90
+ [b'oo', b'bar']
91
+
92
+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 3))
93
+ [b'bar']
94
+
95
+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 6))
96
+ []
97
+
98
+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 7))
99
+ []
100
+
101
+ >>> list(UploadService.shift_chunks([], 0))
102
+ []
103
+ """
104
+
105
+ if offset < 0:
106
+ raise ValueError(f"Expect non-negative offset but got {offset}")
83
107
 
84
108
  for chunk in chunks:
85
109
  if offset:
@@ -112,26 +136,20 @@ class UploadService:
112
136
  return self.upload_shifted_chunks(shifted_chunks, offset)
113
137
 
114
138
  def upload_shifted_chunks(
115
- self,
116
- shifted_chunks: T.Iterable[bytes],
117
- offset: int,
139
+ self, shifted_chunks: T.Iterable[bytes], offset: int
118
140
  ) -> str:
119
141
  """
120
- Upload the chunks that must already be shifted by the offset (e.g. fp.seek(begin_offset, io.SEEK_SET))
142
+ Upload the chunks that must already be shifted by the offset (e.g. fp.seek(offset, io.SEEK_SET))
121
143
  """
122
144
 
123
145
  headers = {
124
146
  "Authorization": f"OAuth {self.user_access_token}",
125
147
  "Offset": f"{offset}",
126
148
  "X-Entity-Name": self.session_key,
127
- "X-Entity-Type": self.MIME_BY_CLUSTER_TYPE[self.cluster_filetype],
128
149
  }
129
150
  url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
130
151
  resp = request_post(
131
- url,
132
- headers=headers,
133
- data=shifted_chunks,
134
- timeout=UPLOAD_REQUESTS_TIMEOUT,
152
+ url, headers=headers, data=shifted_chunks, timeout=UPLOAD_REQUESTS_TIMEOUT
135
153
  )
136
154
 
137
155
  resp.raise_for_status()
@@ -147,18 +165,35 @@ class UploadService:
147
165
 
148
166
  # A mock class for testing only
149
167
  class FakeUploadService(UploadService):
150
- def __init__(self, *args, **kwargs):
168
+ """
169
+ A mock upload service that simulates the upload process for testing purposes.
170
+ It writes the uploaded data to a file in a temporary directory and generates a fake file handle.
171
+ """
172
+
173
+ FILE_HANDLE_DIR: str = "file_handles"
174
+
175
+ def __init__(
176
+ self,
177
+ upload_path: Path | None = None,
178
+ transient_error_ratio: float = 0.0,
179
+ *args,
180
+ **kwargs,
181
+ ):
151
182
  super().__init__(*args, **kwargs)
152
- self._upload_path = os.getenv(
153
- "MAPILLARY_UPLOAD_PATH", "mapillary_public_uploads"
154
- )
155
- self._error_ratio = 0.02
183
+ if upload_path is None:
184
+ upload_path = Path(tempfile.gettempdir()).joinpath(
185
+ "mapillary_public_uploads"
186
+ )
187
+ self._upload_path = upload_path
188
+ self._transient_error_ratio = transient_error_ratio
189
+
190
+ @property
191
+ def upload_path(self) -> Path:
192
+ return self._upload_path
156
193
 
157
194
  @override
158
195
  def upload_shifted_chunks(
159
- self,
160
- shifted_chunks: T.Iterable[bytes],
161
- offset: int,
196
+ self, shifted_chunks: T.Iterable[bytes], offset: int
162
197
  ) -> str:
163
198
  expected_offset = self.fetch_offset()
164
199
  if offset != expected_offset:
@@ -167,28 +202,36 @@ class FakeUploadService(UploadService):
167
202
  )
168
203
 
169
204
  os.makedirs(self._upload_path, exist_ok=True)
170
- filename = os.path.join(self._upload_path, self.session_key)
171
- with open(filename, "ab") as fp:
205
+ filename = self._upload_path.joinpath(self.session_key)
206
+ with filename.open("ab") as fp:
172
207
  for chunk in shifted_chunks:
173
- if random.random() <= self._error_ratio:
208
+ if random.random() <= self._transient_error_ratio:
174
209
  raise requests.ConnectionError(
175
- f"TEST ONLY: Failed to upload with error ratio {self._error_ratio}"
210
+ f"TEST ONLY: Failed to upload with error ratio {self._transient_error_ratio}"
176
211
  )
177
212
  fp.write(chunk)
178
- if random.random() <= self._error_ratio:
213
+ if random.random() <= self._transient_error_ratio:
179
214
  raise requests.ConnectionError(
180
- f"TEST ONLY: Partially uploaded with error ratio {self._error_ratio}"
215
+ f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
181
216
  )
182
- return uuid.uuid4().hex
217
+
218
+ file_handle_dir = self._upload_path.joinpath(self.FILE_HANDLE_DIR)
219
+ file_handle_path = file_handle_dir.joinpath(self.session_key)
220
+ if not file_handle_path.exists():
221
+ os.makedirs(file_handle_dir, exist_ok=True)
222
+ random_file_handle = uuid.uuid4().hex
223
+ file_handle_path.write_text(random_file_handle)
224
+
225
+ return file_handle_path.read_text()
183
226
 
184
227
  @override
185
228
  def fetch_offset(self) -> int:
186
- if random.random() <= self._error_ratio:
229
+ if random.random() <= self._transient_error_ratio:
187
230
  raise requests.ConnectionError(
188
- f"TEST ONLY: Partially uploaded with error ratio {self._error_ratio}"
231
+ f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
189
232
  )
190
- filename = os.path.join(self._upload_path, self.session_key)
191
- if not os.path.exists(filename):
233
+ filename = self._upload_path.joinpath(self.session_key)
234
+ if not filename.exists():
192
235
  return 0
193
236
  with open(filename, "rb") as fp:
194
237
  fp.seek(0, io.SEEK_END)