mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0a1__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 (64) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +106 -7
  3. mapillary_tools/authenticate.py +325 -64
  4. mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +425 -177
  7. mapillary_tools/commands/__main__.py +2 -0
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +18 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +18 -9
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +28 -12
  15. mapillary_tools/constants.py +46 -4
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +158 -53
  18. mapillary_tools/exiftool_read.py +19 -5
  19. mapillary_tools/exiftool_read_video.py +12 -1
  20. mapillary_tools/exiftool_runner.py +77 -0
  21. mapillary_tools/geo.py +148 -107
  22. mapillary_tools/geotag/factory.py +298 -0
  23. mapillary_tools/geotag/geotag_from_generic.py +152 -11
  24. mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
  25. mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
  26. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
  27. mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
  28. mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
  29. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
  30. mapillary_tools/geotag/geotag_images_from_video.py +46 -46
  31. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
  32. mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
  33. mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
  34. mapillary_tools/geotag/options.py +159 -0
  35. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
  36. mapillary_tools/history.py +3 -11
  37. mapillary_tools/mp4/io_utils.py +0 -1
  38. mapillary_tools/mp4/mp4_sample_parser.py +11 -3
  39. mapillary_tools/mp4/simple_mp4_parser.py +0 -10
  40. mapillary_tools/process_geotag_properties.py +151 -386
  41. mapillary_tools/process_sequence_properties.py +554 -202
  42. mapillary_tools/sample_video.py +8 -15
  43. mapillary_tools/telemetry.py +24 -12
  44. mapillary_tools/types.py +80 -22
  45. mapillary_tools/upload.py +311 -261
  46. mapillary_tools/upload_api_v4.py +55 -95
  47. mapillary_tools/uploader.py +396 -254
  48. mapillary_tools/utils.py +26 -0
  49. mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
  50. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
  51. mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
  52. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
  53. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
  54. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
  55. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
  56. mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
  57. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
  58. mapillary_tools/geotag/utils.py +0 -26
  59. mapillary_tools-0.13.3.dist-info/RECORD +0 -75
  60. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  61. /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
  62. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
  63. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
  64. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,24 @@
1
- import enum
1
+ from __future__ import annotations
2
+
2
3
  import io
3
4
  import os
4
5
  import random
6
+ import sys
5
7
  import typing as T
6
8
  import uuid
7
9
 
10
+ if sys.version_info >= (3, 12):
11
+ from typing import override
12
+ else:
13
+ from typing_extensions import override
14
+
8
15
  import requests
9
16
 
10
- from .api_v4 import (
11
- MAPILLARY_GRAPH_API_ENDPOINT,
12
- request_get,
13
- request_post,
14
- REQUESTS_TIMEOUT,
15
- )
17
+ from .api_v4 import ClusterFileType, request_get, request_post, REQUESTS_TIMEOUT
16
18
 
17
19
  MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
18
20
  "MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
19
21
  )
20
- DEFAULT_CHUNK_SIZE = 1024 * 1024 * 16 # 16MB
21
22
  # According to the docs, UPLOAD_REQUESTS_TIMEOUT can be a tuple of
22
23
  # (connection_timeout, read_timeout): https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
23
24
  # In my test, however, the connection_timeout rules both connection timeout and read timeout.
@@ -27,21 +28,12 @@ DEFAULT_CHUNK_SIZE = 1024 * 1024 * 16 # 16MB
27
28
  UPLOAD_REQUESTS_TIMEOUT = (30 * 60, 30 * 60) # 30 minutes
28
29
 
29
30
 
30
- class ClusterFileType(enum.Enum):
31
- ZIP = "zip"
32
- BLACKVUE = "mly_blackvue_video"
33
- CAMM = "mly_camm_video"
34
-
35
-
36
31
  class UploadService:
37
32
  user_access_token: str
38
33
  session_key: str
39
- callbacks: T.List[T.Callable[[bytes, T.Optional[requests.Response]], None]]
40
34
  cluster_filetype: ClusterFileType
41
- organization_id: T.Optional[T.Union[str, int]]
42
- chunk_size: int
43
35
 
44
- MIME_BY_CLUSTER_TYPE: T.Dict[ClusterFileType, str] = {
36
+ MIME_BY_CLUSTER_TYPE: dict[ClusterFileType, str] = {
45
37
  ClusterFileType.ZIP: "application/zip",
46
38
  ClusterFileType.BLACKVUE: "video/mp4",
47
39
  ClusterFileType.CAMM: "video/mp4",
@@ -51,20 +43,12 @@ class UploadService:
51
43
  self,
52
44
  user_access_token: str,
53
45
  session_key: str,
54
- organization_id: T.Optional[T.Union[str, int]] = None,
55
- cluster_filetype: ClusterFileType = ClusterFileType.ZIP,
56
- chunk_size: int = DEFAULT_CHUNK_SIZE,
46
+ cluster_filetype: ClusterFileType,
57
47
  ):
58
- if chunk_size <= 0:
59
- raise ValueError("Expect positive chunk size")
60
-
61
48
  self.user_access_token = user_access_token
62
49
  self.session_key = session_key
63
- self.organization_id = organization_id
64
- # validate the input
65
- self.cluster_filetype = ClusterFileType(cluster_filetype)
66
- self.callbacks = []
67
- self.chunk_size = chunk_size
50
+ # Validate the input
51
+ self.cluster_filetype = cluster_filetype
68
52
 
69
53
  def fetch_offset(self) -> int:
70
54
  headers = {
@@ -80,24 +64,19 @@ class UploadService:
80
64
  data = resp.json()
81
65
  return data["offset"]
82
66
 
83
- def upload(
84
- self,
85
- data: T.IO[bytes],
86
- offset: T.Optional[int] = None,
87
- ) -> str:
88
- chunks = self._chunkize_byte_stream(data)
89
- return self.upload_chunks(chunks, offset=offset)
90
-
91
- def _chunkize_byte_stream(
92
- self, stream: T.IO[bytes]
67
+ @classmethod
68
+ def chunkize_byte_stream(
69
+ cls, stream: T.IO[bytes], chunk_size: int
93
70
  ) -> T.Generator[bytes, None, None]:
71
+ if chunk_size <= 0:
72
+ raise ValueError("Expect positive chunk size")
94
73
  while True:
95
- data = stream.read(self.chunk_size)
74
+ data = stream.read(chunk_size)
96
75
  if not data:
97
76
  break
98
77
  yield data
99
78
 
100
- def _offset_chunks(
79
+ def shift_chunks(
101
80
  self, chunks: T.Iterable[bytes], offset: int
102
81
  ) -> T.Generator[bytes, None, None]:
103
82
  assert offset >= 0, f"Expect non-negative offset but got {offset}"
@@ -112,23 +91,34 @@ class UploadService:
112
91
  else:
113
92
  yield chunk
114
93
 
115
- def _attach_callbacks(
116
- self, chunks: T.Iterable[bytes]
117
- ) -> T.Generator[bytes, None, None]:
118
- for chunk in chunks:
119
- yield chunk
120
- for callback in self.callbacks:
121
- callback(chunk, None)
94
+ def upload_byte_stream(
95
+ self,
96
+ stream: T.IO[bytes],
97
+ offset: int | None = None,
98
+ chunk_size: int = 2 * 1024 * 1024, # 2MB
99
+ ) -> str:
100
+ if offset is None:
101
+ offset = self.fetch_offset()
102
+ return self.upload_chunks(self.chunkize_byte_stream(stream, chunk_size), offset)
122
103
 
123
104
  def upload_chunks(
124
105
  self,
125
106
  chunks: T.Iterable[bytes],
126
- offset: T.Optional[int] = None,
107
+ offset: int | None = None,
127
108
  ) -> str:
128
109
  if offset is None:
129
110
  offset = self.fetch_offset()
111
+ shifted_chunks = self.shift_chunks(chunks, offset)
112
+ return self.upload_shifted_chunks(shifted_chunks, offset)
130
113
 
131
- chunks = self._attach_callbacks(self._offset_chunks(chunks, offset))
114
+ def upload_shifted_chunks(
115
+ self,
116
+ shifted_chunks: T.Iterable[bytes],
117
+ offset: int,
118
+ ) -> str:
119
+ """
120
+ Upload the chunks that must already be shifted by the offset (e.g. fp.seek(begin_offset, io.SEEK_SET))
121
+ """
132
122
 
133
123
  headers = {
134
124
  "Authorization": f"OAuth {self.user_access_token}",
@@ -140,10 +130,12 @@ class UploadService:
140
130
  resp = request_post(
141
131
  url,
142
132
  headers=headers,
143
- data=chunks,
133
+ data=shifted_chunks,
144
134
  timeout=UPLOAD_REQUESTS_TIMEOUT,
145
135
  )
146
136
 
137
+ resp.raise_for_status()
138
+
147
139
  payload = resp.json()
148
140
  try:
149
141
  return payload["h"]
@@ -152,38 +144,6 @@ class UploadService:
152
144
  f"Upload server error: File handle not found in the upload response {resp.text}"
153
145
  )
154
146
 
155
- def finish(self, file_handle: str) -> str:
156
- headers = {
157
- "Authorization": f"OAuth {self.user_access_token}",
158
- }
159
- data: T.Dict[str, T.Union[str, int]] = {
160
- "file_handle": file_handle,
161
- "file_type": self.cluster_filetype.value,
162
- }
163
- if self.organization_id is not None:
164
- data["organization_id"] = self.organization_id
165
-
166
- url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
167
-
168
- resp = request_post(
169
- url,
170
- headers=headers,
171
- json=data,
172
- timeout=REQUESTS_TIMEOUT,
173
- )
174
-
175
- resp.raise_for_status()
176
-
177
- data = resp.json()
178
-
179
- cluster_id = data.get("cluster_id")
180
- if cluster_id is None:
181
- raise RuntimeError(
182
- f"Upload server error: failed to create the cluster {resp.text}"
183
- )
184
-
185
- return T.cast(str, cluster_id)
186
-
187
147
 
188
148
  # A mock class for testing only
189
149
  class FakeUploadService(UploadService):
@@ -192,22 +152,24 @@ class FakeUploadService(UploadService):
192
152
  self._upload_path = os.getenv(
193
153
  "MAPILLARY_UPLOAD_PATH", "mapillary_public_uploads"
194
154
  )
195
- self._error_ratio = 0.1
155
+ self._error_ratio = 0.02
196
156
 
197
- def upload_chunks(
157
+ @override
158
+ def upload_shifted_chunks(
198
159
  self,
199
- chunks: T.Iterable[bytes],
200
- offset: T.Optional[int] = None,
160
+ shifted_chunks: T.Iterable[bytes],
161
+ offset: int,
201
162
  ) -> str:
202
- if offset is None:
203
- offset = self.fetch_offset()
204
-
205
- chunks = self._attach_callbacks(self._offset_chunks(chunks, offset))
163
+ expected_offset = self.fetch_offset()
164
+ if offset != expected_offset:
165
+ raise ValueError(
166
+ f"Expect offset {expected_offset} but got {offset} for session {self.session_key}"
167
+ )
206
168
 
207
169
  os.makedirs(self._upload_path, exist_ok=True)
208
170
  filename = os.path.join(self._upload_path, self.session_key)
209
171
  with open(filename, "ab") as fp:
210
- for chunk in chunks:
172
+ for chunk in shifted_chunks:
211
173
  if random.random() <= self._error_ratio:
212
174
  raise requests.ConnectionError(
213
175
  f"TEST ONLY: Failed to upload with error ratio {self._error_ratio}"
@@ -219,9 +181,7 @@ class FakeUploadService(UploadService):
219
181
  )
220
182
  return uuid.uuid4().hex
221
183
 
222
- def finish(self, _: str) -> str:
223
- return "0"
224
-
184
+ @override
225
185
  def fetch_offset(self) -> int:
226
186
  if random.random() <= self._error_ratio:
227
187
  raise requests.ConnectionError(