mapillary-tools 0.14.0b1__py3-none-any.whl → 0.14.2__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 +66 -263
- mapillary_tools/authenticate.py +47 -39
- mapillary_tools/commands/__main__.py +15 -16
- mapillary_tools/commands/upload.py +33 -4
- mapillary_tools/config.py +5 -0
- mapillary_tools/constants.py +127 -45
- mapillary_tools/exceptions.py +4 -0
- mapillary_tools/exif_read.py +2 -1
- mapillary_tools/exif_write.py +3 -1
- mapillary_tools/geo.py +16 -0
- mapillary_tools/geotag/base.py +6 -2
- mapillary_tools/geotag/factory.py +9 -1
- mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
- mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
- mapillary_tools/geotag/options.py +4 -1
- mapillary_tools/geotag/utils.py +9 -12
- mapillary_tools/geotag/video_extractors/gpx.py +2 -1
- mapillary_tools/geotag/video_extractors/native.py +25 -0
- mapillary_tools/history.py +124 -7
- mapillary_tools/http.py +211 -0
- mapillary_tools/mp4/construct_mp4_parser.py +8 -2
- mapillary_tools/process_geotag_properties.py +35 -38
- mapillary_tools/process_sequence_properties.py +339 -322
- mapillary_tools/sample_video.py +1 -2
- mapillary_tools/serializer/description.py +68 -58
- mapillary_tools/serializer/gpx.py +1 -1
- mapillary_tools/upload.py +202 -207
- mapillary_tools/upload_api_v4.py +57 -47
- mapillary_tools/uploader.py +728 -285
- mapillary_tools/utils.py +57 -5
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/METADATA +7 -6
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/RECORD +38 -37
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/WHEEL +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0b1.dist-info → mapillary_tools-0.14.2.dist-info}/top_level.txt +0 -0
mapillary_tools/upload_api_v4.py
CHANGED
|
@@ -17,18 +17,11 @@ import tempfile
|
|
|
17
17
|
|
|
18
18
|
import requests
|
|
19
19
|
|
|
20
|
-
from .api_v4 import
|
|
20
|
+
from .api_v4 import HTTPContentError, jsonify_response, REQUESTS_TIMEOUT
|
|
21
21
|
|
|
22
22
|
MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
|
|
23
23
|
"MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
|
|
24
24
|
)
|
|
25
|
-
# According to the docs, UPLOAD_REQUESTS_TIMEOUT can be a tuple of
|
|
26
|
-
# (connection_timeout, read_timeout): https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
|
|
27
|
-
# In my test, however, the connection_timeout rules both connection timeout and read timeout.
|
|
28
|
-
# i.e. if your the server does not respond within this timeout, it will throw:
|
|
29
|
-
# ConnectionError: ('Connection aborted.', timeout('The write operation timed out'))
|
|
30
|
-
# So let us make sure the largest possible chunks can be uploaded before this timeout for now,
|
|
31
|
-
UPLOAD_REQUESTS_TIMEOUT = (30 * 60, 30 * 60) # 30 minutes
|
|
32
25
|
|
|
33
26
|
|
|
34
27
|
class UploadService:
|
|
@@ -39,19 +32,21 @@ class UploadService:
|
|
|
39
32
|
user_access_token: str
|
|
40
33
|
session_key: str
|
|
41
34
|
|
|
42
|
-
def __init__(self,
|
|
43
|
-
self.
|
|
35
|
+
def __init__(self, user_session: requests.Session, session_key: str):
|
|
36
|
+
self.user_session = user_session
|
|
44
37
|
self.session_key = session_key
|
|
45
38
|
|
|
46
39
|
def fetch_offset(self) -> int:
|
|
47
|
-
headers = {
|
|
48
|
-
"Authorization": f"OAuth {self.user_access_token}",
|
|
49
|
-
}
|
|
50
40
|
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
51
|
-
|
|
41
|
+
|
|
42
|
+
resp = self.user_session.get(url, timeout=REQUESTS_TIMEOUT)
|
|
52
43
|
resp.raise_for_status()
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
|
|
45
|
+
data = jsonify_response(resp)
|
|
46
|
+
try:
|
|
47
|
+
return data["offset"]
|
|
48
|
+
except KeyError:
|
|
49
|
+
raise HTTPContentError("Offset not found in the response", resp)
|
|
55
50
|
|
|
56
51
|
@classmethod
|
|
57
52
|
def chunkize_byte_stream(
|
|
@@ -120,47 +115,58 @@ class UploadService:
|
|
|
120
115
|
stream: T.IO[bytes],
|
|
121
116
|
offset: int | None = None,
|
|
122
117
|
chunk_size: int = 2 * 1024 * 1024, # 2MB
|
|
118
|
+
read_timeout: float | None = None,
|
|
123
119
|
) -> str:
|
|
124
120
|
if offset is None:
|
|
125
121
|
offset = self.fetch_offset()
|
|
126
|
-
return self.upload_chunks(
|
|
122
|
+
return self.upload_chunks(
|
|
123
|
+
self.chunkize_byte_stream(stream, chunk_size),
|
|
124
|
+
offset,
|
|
125
|
+
read_timeout=read_timeout,
|
|
126
|
+
)
|
|
127
127
|
|
|
128
128
|
def upload_chunks(
|
|
129
129
|
self,
|
|
130
130
|
chunks: T.Iterable[bytes],
|
|
131
131
|
offset: int | None = None,
|
|
132
|
+
read_timeout: float | None = None,
|
|
132
133
|
) -> str:
|
|
133
134
|
if offset is None:
|
|
134
135
|
offset = self.fetch_offset()
|
|
135
136
|
shifted_chunks = self.shift_chunks(chunks, offset)
|
|
136
|
-
return self.upload_shifted_chunks(
|
|
137
|
+
return self.upload_shifted_chunks(
|
|
138
|
+
shifted_chunks, offset, read_timeout=read_timeout
|
|
139
|
+
)
|
|
137
140
|
|
|
138
141
|
def upload_shifted_chunks(
|
|
139
|
-
self,
|
|
142
|
+
self,
|
|
143
|
+
shifted_chunks: T.Iterable[bytes],
|
|
144
|
+
offset: int,
|
|
145
|
+
read_timeout: float | None = None,
|
|
140
146
|
) -> str:
|
|
141
147
|
"""
|
|
142
148
|
Upload the chunks that must already be shifted by the offset (e.g. fp.seek(offset, io.SEEK_SET))
|
|
143
149
|
"""
|
|
144
150
|
|
|
151
|
+
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
145
152
|
headers = {
|
|
146
|
-
"Authorization": f"OAuth {self.user_access_token}",
|
|
147
153
|
"Offset": f"{offset}",
|
|
148
154
|
"X-Entity-Name": self.session_key,
|
|
149
155
|
}
|
|
150
|
-
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
151
|
-
resp = request_post(
|
|
152
|
-
url, headers=headers, data=shifted_chunks, timeout=UPLOAD_REQUESTS_TIMEOUT
|
|
153
|
-
)
|
|
154
156
|
|
|
157
|
+
resp = self.user_session.post(
|
|
158
|
+
url,
|
|
159
|
+
headers=headers,
|
|
160
|
+
data=shifted_chunks,
|
|
161
|
+
timeout=(REQUESTS_TIMEOUT, read_timeout), # type: ignore
|
|
162
|
+
)
|
|
155
163
|
resp.raise_for_status()
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
data = jsonify_response(resp)
|
|
158
166
|
try:
|
|
159
|
-
return
|
|
167
|
+
return data["h"]
|
|
160
168
|
except KeyError:
|
|
161
|
-
raise
|
|
162
|
-
f"Upload server error: File handle not found in the upload response {resp.text}"
|
|
163
|
-
)
|
|
169
|
+
raise HTTPContentError("File handle not found in the response", resp)
|
|
164
170
|
|
|
165
171
|
|
|
166
172
|
# A mock class for testing only
|
|
@@ -174,9 +180,9 @@ class FakeUploadService(UploadService):
|
|
|
174
180
|
|
|
175
181
|
def __init__(
|
|
176
182
|
self,
|
|
183
|
+
*args,
|
|
177
184
|
upload_path: Path | None = None,
|
|
178
185
|
transient_error_ratio: float = 0.0,
|
|
179
|
-
*args,
|
|
180
186
|
**kwargs,
|
|
181
187
|
):
|
|
182
188
|
super().__init__(*args, **kwargs)
|
|
@@ -187,13 +193,12 @@ class FakeUploadService(UploadService):
|
|
|
187
193
|
self._upload_path = upload_path
|
|
188
194
|
self._transient_error_ratio = transient_error_ratio
|
|
189
195
|
|
|
190
|
-
@property
|
|
191
|
-
def upload_path(self) -> Path:
|
|
192
|
-
return self._upload_path
|
|
193
|
-
|
|
194
196
|
@override
|
|
195
197
|
def upload_shifted_chunks(
|
|
196
|
-
self,
|
|
198
|
+
self,
|
|
199
|
+
shifted_chunks: T.Iterable[bytes],
|
|
200
|
+
offset: int,
|
|
201
|
+
read_timeout: float | None = None,
|
|
197
202
|
) -> str:
|
|
198
203
|
expected_offset = self.fetch_offset()
|
|
199
204
|
if offset != expected_offset:
|
|
@@ -205,15 +210,9 @@ class FakeUploadService(UploadService):
|
|
|
205
210
|
filename = self._upload_path.joinpath(self.session_key)
|
|
206
211
|
with filename.open("ab") as fp:
|
|
207
212
|
for chunk in shifted_chunks:
|
|
208
|
-
|
|
209
|
-
raise requests.ConnectionError(
|
|
210
|
-
f"TEST ONLY: Failed to upload with error ratio {self._transient_error_ratio}"
|
|
211
|
-
)
|
|
213
|
+
self._randomly_raise_transient_error()
|
|
212
214
|
fp.write(chunk)
|
|
213
|
-
|
|
214
|
-
raise requests.ConnectionError(
|
|
215
|
-
f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
|
|
216
|
-
)
|
|
215
|
+
self._randomly_raise_transient_error()
|
|
217
216
|
|
|
218
217
|
file_handle_dir = self._upload_path.joinpath(self.FILE_HANDLE_DIR)
|
|
219
218
|
file_handle_path = file_handle_dir.joinpath(self.session_key)
|
|
@@ -226,13 +225,24 @@ class FakeUploadService(UploadService):
|
|
|
226
225
|
|
|
227
226
|
@override
|
|
228
227
|
def fetch_offset(self) -> int:
|
|
229
|
-
|
|
230
|
-
raise requests.ConnectionError(
|
|
231
|
-
f"TEST ONLY: Partially uploaded with error ratio {self._transient_error_ratio}"
|
|
232
|
-
)
|
|
228
|
+
self._randomly_raise_transient_error()
|
|
233
229
|
filename = self._upload_path.joinpath(self.session_key)
|
|
234
230
|
if not filename.exists():
|
|
235
231
|
return 0
|
|
236
232
|
with open(filename, "rb") as fp:
|
|
237
233
|
fp.seek(0, io.SEEK_END)
|
|
238
234
|
return fp.tell()
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def upload_path(self) -> Path:
|
|
238
|
+
return self._upload_path
|
|
239
|
+
|
|
240
|
+
def _randomly_raise_transient_error(self):
|
|
241
|
+
"""
|
|
242
|
+
Randomly raise a transient error based on the configured error ratio.
|
|
243
|
+
This is for testing purposes only.
|
|
244
|
+
"""
|
|
245
|
+
if random.random() <= self._transient_error_ratio:
|
|
246
|
+
raise requests.ConnectionError(
|
|
247
|
+
f"[TEST ONLY]: Transient error with ratio {self._transient_error_ratio}"
|
|
248
|
+
)
|