mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a2__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 +237 -16
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +12 -6
- 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 +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +31 -13
- mapillary_tools/constants.py +47 -6
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +46 -33
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/ffmpeg.py +24 -23
- mapillary_tools/geo.py +144 -120
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +291 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
- mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
- 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 +53 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -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 +160 -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/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
- mapillary_tools/geotag/video_extractors/native.py +157 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +7 -13
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- 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 +155 -392
- mapillary_tools/process_sequence_properties.py +562 -208
- mapillary_tools/sample_video.py +13 -20
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +111 -58
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +42 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/__init__.py +0 -1
- 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/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.0a2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
mapillary_tools/upload_api_v4.py
CHANGED
|
@@ -1,68 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import io
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
4
|
import os
|
|
6
5
|
import random
|
|
6
|
+
import sys
|
|
7
7
|
import typing as T
|
|
8
8
|
import uuid
|
|
9
9
|
|
|
10
|
+
if sys.version_info >= (3, 12):
|
|
11
|
+
from typing import override
|
|
12
|
+
else:
|
|
13
|
+
from typing_extensions import override
|
|
14
|
+
|
|
10
15
|
import requests
|
|
11
16
|
|
|
12
|
-
from .api_v4 import
|
|
17
|
+
from .api_v4 import ClusterFileType, request_get, request_post, REQUESTS_TIMEOUT
|
|
13
18
|
|
|
14
|
-
LOG = logging.getLogger(__name__)
|
|
15
19
|
MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
|
|
16
20
|
"MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
|
|
17
21
|
)
|
|
18
|
-
DEFAULT_CHUNK_SIZE = 1024 * 1024 * 16 # 16MB
|
|
19
22
|
# According to the docs, UPLOAD_REQUESTS_TIMEOUT can be a tuple of
|
|
20
23
|
# (connection_timeout, read_timeout): https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
|
|
21
24
|
# In my test, however, the connection_timeout rules both connection timeout and read timeout.
|
|
22
25
|
# i.e. if your the server does not respond within this timeout, it will throw:
|
|
23
26
|
# ConnectionError: ('Connection aborted.', timeout('The write operation timed out'))
|
|
24
27
|
# So let us make sure the largest possible chunks can be uploaded before this timeout for now,
|
|
25
|
-
REQUESTS_TIMEOUT = (20, 20) # 20 seconds
|
|
26
28
|
UPLOAD_REQUESTS_TIMEOUT = (30 * 60, 30 * 60) # 30 minutes
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
class ClusterFileType(enum.Enum):
|
|
30
|
-
ZIP = "zip"
|
|
31
|
-
BLACKVUE = "mly_blackvue_video"
|
|
32
|
-
CAMM = "mly_camm_video"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _sanitize_headers(headers: T.Dict):
|
|
36
|
-
return {
|
|
37
|
-
k: v
|
|
38
|
-
for k, v in headers.items()
|
|
39
|
-
if k.lower() not in ["authorization", "cookie", "x-fb-access-token"]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
_S = T.TypeVar("_S", str, bytes)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _truncate_end(s: _S) -> _S:
|
|
47
|
-
MAX_LENGTH = 512
|
|
48
|
-
if MAX_LENGTH < len(s):
|
|
49
|
-
if isinstance(s, bytes):
|
|
50
|
-
return s[:MAX_LENGTH] + b"..."
|
|
51
|
-
else:
|
|
52
|
-
return str(s[:MAX_LENGTH]) + "..."
|
|
53
|
-
else:
|
|
54
|
-
return s
|
|
55
|
-
|
|
56
|
-
|
|
57
31
|
class UploadService:
|
|
58
32
|
user_access_token: str
|
|
59
33
|
session_key: str
|
|
60
|
-
callbacks: T.List[T.Callable[[bytes, T.Optional[requests.Response]], None]]
|
|
61
34
|
cluster_filetype: ClusterFileType
|
|
62
|
-
organization_id: T.Optional[T.Union[str, int]]
|
|
63
|
-
chunk_size: int
|
|
64
35
|
|
|
65
|
-
MIME_BY_CLUSTER_TYPE:
|
|
36
|
+
MIME_BY_CLUSTER_TYPE: dict[ClusterFileType, str] = {
|
|
66
37
|
ClusterFileType.ZIP: "application/zip",
|
|
67
38
|
ClusterFileType.BLACKVUE: "video/mp4",
|
|
68
39
|
ClusterFileType.CAMM: "video/mp4",
|
|
@@ -72,55 +43,40 @@ class UploadService:
|
|
|
72
43
|
self,
|
|
73
44
|
user_access_token: str,
|
|
74
45
|
session_key: str,
|
|
75
|
-
|
|
76
|
-
cluster_filetype: ClusterFileType = ClusterFileType.ZIP,
|
|
77
|
-
chunk_size: int = DEFAULT_CHUNK_SIZE,
|
|
46
|
+
cluster_filetype: ClusterFileType,
|
|
78
47
|
):
|
|
79
|
-
if chunk_size <= 0:
|
|
80
|
-
raise ValueError("Expect positive chunk size")
|
|
81
|
-
|
|
82
48
|
self.user_access_token = user_access_token
|
|
83
49
|
self.session_key = session_key
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
self.cluster_filetype = ClusterFileType(cluster_filetype)
|
|
87
|
-
self.callbacks = []
|
|
88
|
-
self.chunk_size = chunk_size
|
|
50
|
+
# Validate the input
|
|
51
|
+
self.cluster_filetype = cluster_filetype
|
|
89
52
|
|
|
90
53
|
def fetch_offset(self) -> int:
|
|
91
54
|
headers = {
|
|
92
55
|
"Authorization": f"OAuth {self.user_access_token}",
|
|
93
56
|
}
|
|
94
57
|
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
95
|
-
LOG.debug("GET %s", url)
|
|
96
58
|
resp = request_get(
|
|
97
59
|
url,
|
|
98
60
|
headers=headers,
|
|
99
61
|
timeout=REQUESTS_TIMEOUT,
|
|
100
62
|
)
|
|
101
|
-
LOG.debug("HTTP response %s: %s", resp.status_code, resp.content)
|
|
102
63
|
resp.raise_for_status()
|
|
103
64
|
data = resp.json()
|
|
104
65
|
return data["offset"]
|
|
105
66
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
offset: T.Optional[int] = None,
|
|
110
|
-
) -> str:
|
|
111
|
-
chunks = self._chunkize_byte_stream(data)
|
|
112
|
-
return self.upload_chunks(chunks, offset=offset)
|
|
113
|
-
|
|
114
|
-
def _chunkize_byte_stream(
|
|
115
|
-
self, stream: T.IO[bytes]
|
|
67
|
+
@classmethod
|
|
68
|
+
def chunkize_byte_stream(
|
|
69
|
+
cls, stream: T.IO[bytes], chunk_size: int
|
|
116
70
|
) -> T.Generator[bytes, None, None]:
|
|
71
|
+
if chunk_size <= 0:
|
|
72
|
+
raise ValueError("Expect positive chunk size")
|
|
117
73
|
while True:
|
|
118
|
-
data = stream.read(
|
|
74
|
+
data = stream.read(chunk_size)
|
|
119
75
|
if not data:
|
|
120
76
|
break
|
|
121
77
|
yield data
|
|
122
78
|
|
|
123
|
-
def
|
|
79
|
+
def shift_chunks(
|
|
124
80
|
self, chunks: T.Iterable[bytes], offset: int
|
|
125
81
|
) -> T.Generator[bytes, None, None]:
|
|
126
82
|
assert offset >= 0, f"Expect non-negative offset but got {offset}"
|
|
@@ -135,23 +91,34 @@ class UploadService:
|
|
|
135
91
|
else:
|
|
136
92
|
yield chunk
|
|
137
93
|
|
|
138
|
-
def
|
|
139
|
-
self,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
145
103
|
|
|
146
104
|
def upload_chunks(
|
|
147
105
|
self,
|
|
148
106
|
chunks: T.Iterable[bytes],
|
|
149
|
-
offset:
|
|
107
|
+
offset: int | None = None,
|
|
150
108
|
) -> str:
|
|
151
109
|
if offset is None:
|
|
152
110
|
offset = self.fetch_offset()
|
|
111
|
+
shifted_chunks = self.shift_chunks(chunks, offset)
|
|
112
|
+
return self.upload_shifted_chunks(shifted_chunks, offset)
|
|
153
113
|
|
|
154
|
-
|
|
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
|
+
"""
|
|
155
122
|
|
|
156
123
|
headers = {
|
|
157
124
|
"Authorization": f"OAuth {self.user_access_token}",
|
|
@@ -160,14 +127,14 @@ class UploadService:
|
|
|
160
127
|
"X-Entity-Type": self.MIME_BY_CLUSTER_TYPE[self.cluster_filetype],
|
|
161
128
|
}
|
|
162
129
|
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
163
|
-
LOG.debug("POST %s HEADERS %s", url, json.dumps(_sanitize_headers(headers)))
|
|
164
130
|
resp = request_post(
|
|
165
131
|
url,
|
|
166
132
|
headers=headers,
|
|
167
|
-
data=
|
|
133
|
+
data=shifted_chunks,
|
|
168
134
|
timeout=UPLOAD_REQUESTS_TIMEOUT,
|
|
169
135
|
)
|
|
170
|
-
|
|
136
|
+
|
|
137
|
+
resp.raise_for_status()
|
|
171
138
|
|
|
172
139
|
payload = resp.json()
|
|
173
140
|
try:
|
|
@@ -177,40 +144,6 @@ class UploadService:
|
|
|
177
144
|
f"Upload server error: File handle not found in the upload response {resp.text}"
|
|
178
145
|
)
|
|
179
146
|
|
|
180
|
-
def finish(self, file_handle: str) -> str:
|
|
181
|
-
headers = {
|
|
182
|
-
"Authorization": f"OAuth {self.user_access_token}",
|
|
183
|
-
}
|
|
184
|
-
data: T.Dict[str, T.Union[str, int]] = {
|
|
185
|
-
"file_handle": file_handle,
|
|
186
|
-
"file_type": self.cluster_filetype.value,
|
|
187
|
-
}
|
|
188
|
-
if self.organization_id is not None:
|
|
189
|
-
data["organization_id"] = self.organization_id
|
|
190
|
-
|
|
191
|
-
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
|
|
192
|
-
|
|
193
|
-
LOG.debug("POST %s HEADERS %s", url, json.dumps(_sanitize_headers(headers)))
|
|
194
|
-
resp = request_post(
|
|
195
|
-
url,
|
|
196
|
-
headers=headers,
|
|
197
|
-
json=data,
|
|
198
|
-
timeout=REQUESTS_TIMEOUT,
|
|
199
|
-
)
|
|
200
|
-
LOG.debug("HTTP response %s: %s", resp.status_code, _truncate_end(resp.content))
|
|
201
|
-
|
|
202
|
-
resp.raise_for_status()
|
|
203
|
-
|
|
204
|
-
data = resp.json()
|
|
205
|
-
|
|
206
|
-
cluster_id = data.get("cluster_id")
|
|
207
|
-
if cluster_id is None:
|
|
208
|
-
raise RuntimeError(
|
|
209
|
-
f"Upload server error: failed to create the cluster {resp.text}"
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
return T.cast(str, cluster_id)
|
|
213
|
-
|
|
214
147
|
|
|
215
148
|
# A mock class for testing only
|
|
216
149
|
class FakeUploadService(UploadService):
|
|
@@ -219,22 +152,24 @@ class FakeUploadService(UploadService):
|
|
|
219
152
|
self._upload_path = os.getenv(
|
|
220
153
|
"MAPILLARY_UPLOAD_PATH", "mapillary_public_uploads"
|
|
221
154
|
)
|
|
222
|
-
self._error_ratio = 0.
|
|
155
|
+
self._error_ratio = 0.02
|
|
223
156
|
|
|
224
|
-
|
|
157
|
+
@override
|
|
158
|
+
def upload_shifted_chunks(
|
|
225
159
|
self,
|
|
226
|
-
|
|
227
|
-
offset:
|
|
160
|
+
shifted_chunks: T.Iterable[bytes],
|
|
161
|
+
offset: int,
|
|
228
162
|
) -> str:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
)
|
|
233
168
|
|
|
234
169
|
os.makedirs(self._upload_path, exist_ok=True)
|
|
235
170
|
filename = os.path.join(self._upload_path, self.session_key)
|
|
236
171
|
with open(filename, "ab") as fp:
|
|
237
|
-
for chunk in
|
|
172
|
+
for chunk in shifted_chunks:
|
|
238
173
|
if random.random() <= self._error_ratio:
|
|
239
174
|
raise requests.ConnectionError(
|
|
240
175
|
f"TEST ONLY: Failed to upload with error ratio {self._error_ratio}"
|
|
@@ -246,9 +181,7 @@ class FakeUploadService(UploadService):
|
|
|
246
181
|
)
|
|
247
182
|
return uuid.uuid4().hex
|
|
248
183
|
|
|
249
|
-
|
|
250
|
-
return "0"
|
|
251
|
-
|
|
184
|
+
@override
|
|
252
185
|
def fetch_offset(self) -> int:
|
|
253
186
|
if random.random() <= self._error_ratio:
|
|
254
187
|
raise requests.ConnectionError(
|