mapillary-tools 0.14.0a1__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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +5 -4
- mapillary_tools/authenticate.py +9 -9
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/camm/camm_parser.py +5 -5
- mapillary_tools/commands/__main__.py +1 -2
- mapillary_tools/config.py +41 -18
- mapillary_tools/constants.py +3 -2
- mapillary_tools/exceptions.py +1 -1
- mapillary_tools/exif_read.py +65 -65
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +23 -46
- mapillary_tools/exiftool_read_video.py +88 -49
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +4 -21
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
- mapillary_tools/geotag/factory.py +105 -103
- mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
- mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
- mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
- mapillary_tools/geotag/geotag_images_from_video.py +51 -14
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
- mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
- 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 +26 -3
- mapillary_tools/geotag/utils.py +62 -0
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +135 -0
- mapillary_tools/gpmf/gpmf_parser.py +16 -16
- mapillary_tools/gpmf/gps_filter.py +5 -3
- mapillary_tools/history.py +8 -3
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- mapillary_tools/mp4/mp4_sample_parser.py +27 -27
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -12
- mapillary_tools/process_geotag_properties.py +21 -15
- mapillary_tools/process_sequence_properties.py +49 -49
- mapillary_tools/sample_video.py +15 -14
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +6 -5
- mapillary_tools/types.py +64 -635
- mapillary_tools/upload.py +176 -197
- mapillary_tools/upload_api_v4.py +94 -51
- mapillary_tools/uploader.py +284 -138
- mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
- mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- 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.14.0a1.dist-info/RECORD +0 -78
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
mapillary_tools/upload_api_v4.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
81
|
+
cls, chunks: T.Iterable[bytes], offset: int
|
|
81
82
|
) -> T.Generator[bytes, None, None]:
|
|
82
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 =
|
|
171
|
-
with open(
|
|
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.
|
|
208
|
+
if random.random() <= self._transient_error_ratio:
|
|
174
209
|
raise requests.ConnectionError(
|
|
175
|
-
f"TEST ONLY: Failed to upload with error ratio {self.
|
|
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.
|
|
213
|
+
if random.random() <= self._transient_error_ratio:
|
|
179
214
|
raise requests.ConnectionError(
|
|
180
|
-
f"TEST ONLY: Partially uploaded with error ratio {self.
|
|
215
|
+
f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
|
|
181
216
|
)
|
|
182
|
-
|
|
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.
|
|
229
|
+
if random.random() <= self._transient_error_ratio:
|
|
187
230
|
raise requests.ConnectionError(
|
|
188
|
-
f"TEST ONLY: Partially uploaded with error ratio {self.
|
|
231
|
+
f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
|
|
189
232
|
)
|
|
190
|
-
filename =
|
|
191
|
-
if not
|
|
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)
|