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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +106 -7
- 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 +425 -177
- mapillary_tools/commands/__main__.py +2 -0
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +18 -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 +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +311 -261
- mapillary_tools/upload_api_v4.py +55 -95
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
mapillary_tools/upload_api_v4.py
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
74
|
+
data = stream.read(chunk_size)
|
|
96
75
|
if not data:
|
|
97
76
|
break
|
|
98
77
|
yield data
|
|
99
78
|
|
|
100
|
-
def
|
|
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
|
|
116
|
-
self,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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
|
-
|
|
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=
|
|
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.
|
|
155
|
+
self._error_ratio = 0.02
|
|
196
156
|
|
|
197
|
-
|
|
157
|
+
@override
|
|
158
|
+
def upload_shifted_chunks(
|
|
198
159
|
self,
|
|
199
|
-
|
|
200
|
-
offset:
|
|
160
|
+
shifted_chunks: T.Iterable[bytes],
|
|
161
|
+
offset: int,
|
|
201
162
|
) -> str:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
-
|
|
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(
|