mapillary-tools 0.14.0b1__tar.gz → 0.14.2__tar.gz
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-0.14.0b1 → mapillary_tools-0.14.2}/PKG-INFO +7 -6
- mapillary_tools-0.14.2/mapillary_tools/__init__.py +1 -0
- mapillary_tools-0.14.2/mapillary_tools/api_v4.py +176 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/authenticate.py +47 -39
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/__main__.py +15 -16
- mapillary_tools-0.14.2/mapillary_tools/commands/upload.py +88 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/config.py +5 -0
- mapillary_tools-0.14.2/mapillary_tools/constants.py +175 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/exceptions.py +4 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/exif_read.py +2 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/exif_write.py +3 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geo.py +16 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/base.py +6 -2
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/factory.py +9 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/options.py +4 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/utils.py +9 -12
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/video_extractors/gpx.py +2 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/video_extractors/native.py +25 -0
- mapillary_tools-0.14.2/mapillary_tools/history.py +182 -0
- mapillary_tools-0.14.2/mapillary_tools/http.py +211 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/mp4/construct_mp4_parser.py +8 -2
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/process_geotag_properties.py +35 -38
- mapillary_tools-0.14.2/mapillary_tools/process_sequence_properties.py +714 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/sample_video.py +1 -2
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/serializer/description.py +68 -58
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/serializer/gpx.py +1 -1
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/upload.py +202 -207
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/upload_api_v4.py +57 -47
- mapillary_tools-0.14.2/mapillary_tools/uploader.py +1170 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/utils.py +57 -5
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools.egg-info/PKG-INFO +7 -6
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools.egg-info/SOURCES.txt +1 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools.egg-info/requires.txt +6 -5
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/pyproject.toml +6 -5
- mapillary_tools-0.14.0b1/mapillary_tools/__init__.py +0 -1
- mapillary_tools-0.14.0b1/mapillary_tools/api_v4.py +0 -373
- mapillary_tools-0.14.0b1/mapillary_tools/commands/upload.py +0 -59
- mapillary_tools-0.14.0b1/mapillary_tools/constants.py +0 -93
- mapillary_tools-0.14.0b1/mapillary_tools/history.py +0 -65
- mapillary_tools-0.14.0b1/mapillary_tools/process_sequence_properties.py +0 -697
- mapillary_tools-0.14.0b1/mapillary_tools/uploader.py +0 -727
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/LICENSE +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/README.md +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/blackvue_parser.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/camm/camm_builder.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/camm/camm_parser.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/__init__.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/authenticate.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/process.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/process_and_upload.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/sample_video.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/video_process.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/video_process_and_upload.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/commands/zip.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/exiftool_read.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/exiftool_read_video.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/exiftool_runner.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/ffmpeg.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/__init__.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_images_from_exif.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_videos_from_gpx.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/geotag_videos_from_video.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/image_extractors/base.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/image_extractors/exif.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/image_extractors/exiftool.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/video_extractors/base.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/geotag/video_extractors/exiftool.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/gpmf/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/gpmf/gpmf_parser.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/gpmf/gps_filter.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/ipc.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/mp4/__init__.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/mp4/io_utils.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/mp4/mp4_sample_parser.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/mp4/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/telemetry.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools/types.py +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools.egg-info/dependency_links.txt +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools.egg-info/entry_points.txt +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/mapillary_tools.egg-info/top_level.txt +0 -0
- {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mapillary_tools
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2
|
|
4
4
|
Summary: Mapillary Image/Video Import Pipeline
|
|
5
5
|
Author-email: Mapillary <support@mapillary.com>
|
|
6
6
|
License: BSD
|
|
@@ -22,11 +22,12 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: appdirs<2.0.0,>=1.4.4
|
|
25
|
-
Requires-Dist: construct
|
|
26
|
-
Requires-Dist: exifread
|
|
27
|
-
Requires-Dist: gpxpy
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
25
|
+
Requires-Dist: construct~=2.10.0
|
|
26
|
+
Requires-Dist: exifread~=3.0
|
|
27
|
+
Requires-Dist: gpxpy~=1.6.0
|
|
28
|
+
Requires-Dist: humanize>=4.12.3
|
|
29
|
+
Requires-Dist: jsonschema~=4.17.0
|
|
30
|
+
Requires-Dist: piexif~=1.1
|
|
30
31
|
Requires-Dist: pynmea2<2.0.0,>=1.12.0
|
|
31
32
|
Requires-Dist: requests[socks]<3.0.0,>=2.20.0
|
|
32
33
|
Requires-Dist: tqdm<5.0,>=4.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.14.2"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import typing as T
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from . import http
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger(__name__)
|
|
13
|
+
MAPILLARY_CLIENT_TOKEN = os.getenv(
|
|
14
|
+
"MAPILLARY_CLIENT_TOKEN", "MLY|5675152195860640|6b02c72e6e3c801e5603ab0495623282"
|
|
15
|
+
)
|
|
16
|
+
MAPILLARY_GRAPH_API_ENDPOINT = os.getenv(
|
|
17
|
+
"MAPILLARY_GRAPH_API_ENDPOINT", "https://graph.mapillary.com"
|
|
18
|
+
)
|
|
19
|
+
REQUESTS_TIMEOUT: float = 60 # 1 minutes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HTTPContentError(Exception):
|
|
23
|
+
"""
|
|
24
|
+
Raised when the HTTP response is ok (200) but the content is not as expected
|
|
25
|
+
e.g. not JSON or not a valid response.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, response: requests.Response):
|
|
29
|
+
self.response = response
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClusterFileType(enum.Enum):
|
|
34
|
+
ZIP = "zip"
|
|
35
|
+
BLACKVUE = "mly_blackvue_video"
|
|
36
|
+
CAMM = "mly_camm_video"
|
|
37
|
+
MLY_BUNDLE_MANIFEST = "mly_bundle_manifest"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_user_session(user_access_token: str) -> requests.Session:
|
|
41
|
+
session = http.Session()
|
|
42
|
+
session.headers["Authorization"] = f"OAuth {user_access_token}"
|
|
43
|
+
return session
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_client_session(disable_logging: bool = False) -> requests.Session:
|
|
47
|
+
session = http.Session()
|
|
48
|
+
session.headers["Authorization"] = f"OAuth {MAPILLARY_CLIENT_TOKEN}"
|
|
49
|
+
if disable_logging:
|
|
50
|
+
session.disable_logging_request = True
|
|
51
|
+
session.disable_logging_response = True
|
|
52
|
+
return session
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_auth_error(resp: requests.Response) -> bool:
|
|
56
|
+
if resp.status_code in [401, 403]:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
if resp.status_code in [400]:
|
|
60
|
+
try:
|
|
61
|
+
error_body = resp.json()
|
|
62
|
+
except Exception:
|
|
63
|
+
error_body = {}
|
|
64
|
+
|
|
65
|
+
type = error_body.get("debug_info", {}).get("type")
|
|
66
|
+
if type in ["NotAuthorizedError"]:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def extract_auth_error_message(resp: requests.Response) -> str:
|
|
73
|
+
assert is_auth_error(resp), "has to be an auth error"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
error_body = resp.json()
|
|
77
|
+
except Exception:
|
|
78
|
+
error_body = {}
|
|
79
|
+
|
|
80
|
+
# from Graph APIs
|
|
81
|
+
message = error_body.get("error", {}).get("message")
|
|
82
|
+
if message is not None:
|
|
83
|
+
return str(message)
|
|
84
|
+
|
|
85
|
+
# from upload service
|
|
86
|
+
message = error_body.get("debug_info", {}).get("message")
|
|
87
|
+
if message is not None:
|
|
88
|
+
return str(message)
|
|
89
|
+
|
|
90
|
+
return resp.text
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_upload_token(
|
|
94
|
+
client_session: requests.Session, email: str, password: str
|
|
95
|
+
) -> requests.Response:
|
|
96
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/login"
|
|
97
|
+
json_data = {"email": email, "password": password, "locale": "en_US"}
|
|
98
|
+
|
|
99
|
+
resp = client_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
|
|
102
|
+
return resp
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fetch_organization(
|
|
106
|
+
user_session: requests.Session, organization_id: int | str
|
|
107
|
+
) -> requests.Response:
|
|
108
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}"
|
|
109
|
+
params = {"fields": ",".join(["slug", "description", "name"])}
|
|
110
|
+
|
|
111
|
+
resp = user_session.get(url, params=params, timeout=REQUESTS_TIMEOUT)
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
|
|
114
|
+
return resp
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def fetch_user_or_me(
|
|
118
|
+
user_session: requests.Session, user_id: int | str | None = None
|
|
119
|
+
) -> requests.Response:
|
|
120
|
+
if user_id is None:
|
|
121
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/me"
|
|
122
|
+
else:
|
|
123
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/{user_id}"
|
|
124
|
+
params = {"fields": ",".join(["id", "username"])}
|
|
125
|
+
|
|
126
|
+
resp = user_session.get(url, params=params, timeout=REQUESTS_TIMEOUT)
|
|
127
|
+
resp.raise_for_status()
|
|
128
|
+
|
|
129
|
+
return resp
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
ActionType = T.Literal[
|
|
133
|
+
"upload_started_upload", "upload_finished_upload", "upload_failed_upload"
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def log_event(
|
|
138
|
+
client_session: requests.Session, action_type: ActionType, properties: dict
|
|
139
|
+
) -> requests.Response:
|
|
140
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging"
|
|
141
|
+
json_data = {"action_type": action_type, "properties": properties}
|
|
142
|
+
|
|
143
|
+
resp = client_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
|
|
144
|
+
resp.raise_for_status()
|
|
145
|
+
|
|
146
|
+
return resp
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def finish_upload(
|
|
150
|
+
user_session: requests.Session,
|
|
151
|
+
file_handle: str,
|
|
152
|
+
cluster_filetype: ClusterFileType,
|
|
153
|
+
organization_id: int | str | None = None,
|
|
154
|
+
) -> requests.Response:
|
|
155
|
+
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
|
|
156
|
+
json_data: dict[str, str | int] = {
|
|
157
|
+
"file_handle": file_handle,
|
|
158
|
+
"file_type": cluster_filetype.value,
|
|
159
|
+
}
|
|
160
|
+
if organization_id is not None:
|
|
161
|
+
json_data["organization_id"] = organization_id
|
|
162
|
+
|
|
163
|
+
resp = user_session.post(url, json=json_data, timeout=REQUESTS_TIMEOUT)
|
|
164
|
+
resp.raise_for_status()
|
|
165
|
+
|
|
166
|
+
return resp
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def jsonify_response(resp: requests.Response) -> T.Any:
|
|
170
|
+
"""
|
|
171
|
+
Convert the response to JSON, raising HTTPContentError if the response is not JSON.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
return resp.json()
|
|
175
|
+
except requests.JSONDecodeError as ex:
|
|
176
|
+
raise HTTPContentError("Invalid JSON response", resp) from ex
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import getpass
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
import re
|
|
7
6
|
import sys
|
|
@@ -11,7 +10,7 @@ import jsonschema
|
|
|
11
10
|
|
|
12
11
|
import requests
|
|
13
12
|
|
|
14
|
-
from . import api_v4, config, constants, exceptions
|
|
13
|
+
from . import api_v4, config, constants, exceptions, http
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
LOG = logging.getLogger(__name__)
|
|
@@ -78,17 +77,16 @@ def authenticate(
|
|
|
78
77
|
# TODO: print more user information
|
|
79
78
|
if profile_name in all_user_items:
|
|
80
79
|
LOG.info(
|
|
81
|
-
'Profile "%s" updated: %s', profile_name,
|
|
80
|
+
'Profile "%s" updated: %s', profile_name, http._sanitize(user_items)
|
|
82
81
|
)
|
|
83
82
|
else:
|
|
84
83
|
LOG.info(
|
|
85
|
-
'Profile "%s" created: %s', profile_name,
|
|
84
|
+
'Profile "%s" created: %s', profile_name, http._sanitize(user_items)
|
|
86
85
|
)
|
|
87
86
|
|
|
88
87
|
|
|
89
88
|
def fetch_user_items(
|
|
90
|
-
user_name: str | None = None,
|
|
91
|
-
organization_key: str | None = None,
|
|
89
|
+
user_name: str | None = None, organization_key: str | None = None
|
|
92
90
|
) -> config.UserItem:
|
|
93
91
|
"""
|
|
94
92
|
Read user information from the config file,
|
|
@@ -129,18 +127,28 @@ def fetch_user_items(
|
|
|
129
127
|
|
|
130
128
|
assert profile_name is not None, "profile_name should be set"
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'Uploading to profile "%s": %s', profile_name, api_v4._sanitize(user_items)
|
|
136
|
-
)
|
|
130
|
+
try:
|
|
131
|
+
LOG.info(f'Verifying profile "{profile_name}"...')
|
|
132
|
+
user_items = _verify_user_auth(_validate_profile(user_items))
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
user_items["user_upload_token"], organization_key
|
|
134
|
+
LOG.info(
|
|
135
|
+
f'Uploading to profile "{profile_name}": {user_items.get("MAPSettingsUsername")} (ID: {user_items.get("MAPSettingsUserKey")})'
|
|
141
136
|
)
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
|
|
138
|
+
if organization_key is not None:
|
|
139
|
+
with api_v4.create_user_session(user_items["user_upload_token"]) as session:
|
|
140
|
+
resp = api_v4.fetch_organization(session, organization_key)
|
|
141
|
+
data = api_v4.jsonify_response(resp)
|
|
142
|
+
LOG.info(
|
|
143
|
+
f"Uploading to organization: {data.get('name')} (ID: {data.get('id')})"
|
|
144
|
+
)
|
|
145
|
+
user_items["MAPOrganizationKey"] = data.get("id")
|
|
146
|
+
|
|
147
|
+
except requests.Timeout as ex:
|
|
148
|
+
raise exceptions.MapillaryUploadTimeoutError(str(ex)) from ex
|
|
149
|
+
|
|
150
|
+
except requests.ConnectionError as ex:
|
|
151
|
+
raise exceptions.MapillaryUploadConnectionError(str(ex)) from ex
|
|
144
152
|
|
|
145
153
|
return user_items
|
|
146
154
|
|
|
@@ -157,7 +165,7 @@ def _prompt(message: str) -> str:
|
|
|
157
165
|
|
|
158
166
|
def _validate_profile(user_items: config.UserItem) -> config.UserItem:
|
|
159
167
|
try:
|
|
160
|
-
|
|
168
|
+
config.UserItemSchemaValidator.validate(user_items)
|
|
161
169
|
except jsonschema.ValidationError as ex:
|
|
162
170
|
raise exceptions.MapillaryBadParameterError(
|
|
163
171
|
f"Invalid profile format: {ex.message}"
|
|
@@ -172,23 +180,22 @@ def _verify_user_auth(user_items: config.UserItem) -> config.UserItem:
|
|
|
172
180
|
if constants._AUTH_VERIFICATION_DISABLED:
|
|
173
181
|
return user_items
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
raise ex
|
|
183
|
+
with api_v4.create_user_session(user_items["user_upload_token"]) as session:
|
|
184
|
+
try:
|
|
185
|
+
resp = api_v4.fetch_user_or_me(session)
|
|
186
|
+
except requests.HTTPError as ex:
|
|
187
|
+
if api_v4.is_auth_error(ex.response):
|
|
188
|
+
message = api_v4.extract_auth_error_message(ex.response)
|
|
189
|
+
raise exceptions.MapillaryUploadUnauthorizedError(message)
|
|
190
|
+
else:
|
|
191
|
+
raise ex
|
|
185
192
|
|
|
186
|
-
|
|
193
|
+
data = api_v4.jsonify_response(resp)
|
|
187
194
|
|
|
188
195
|
return {
|
|
189
196
|
**user_items,
|
|
190
|
-
"MAPSettingsUsername":
|
|
191
|
-
"MAPSettingsUserKey":
|
|
197
|
+
"MAPSettingsUsername": data.get("username"),
|
|
198
|
+
"MAPSettingsUserKey": data.get("id"),
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
|
|
@@ -275,18 +282,19 @@ def _prompt_login(
|
|
|
275
282
|
if user_password:
|
|
276
283
|
break
|
|
277
284
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
with api_v4.create_client_session() as session:
|
|
286
|
+
try:
|
|
287
|
+
resp = api_v4.get_upload_token(session, user_email, user_password)
|
|
288
|
+
except requests.HTTPError as ex:
|
|
289
|
+
if not _enabled:
|
|
290
|
+
raise ex
|
|
283
291
|
|
|
284
|
-
|
|
285
|
-
|
|
292
|
+
if _is_login_retryable(ex):
|
|
293
|
+
return _prompt_login()
|
|
286
294
|
|
|
287
|
-
|
|
295
|
+
raise ex
|
|
288
296
|
|
|
289
|
-
data =
|
|
297
|
+
data = api_v4.jsonify_response(resp)
|
|
290
298
|
|
|
291
299
|
user_items: config.UserItem = {
|
|
292
300
|
"user_upload_token": str(data["access_token"]),
|
|
@@ -7,6 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
9
9
|
from .. import api_v4, constants, exceptions, VERSION
|
|
10
|
+
from ..upload import log_exception
|
|
11
|
+
from ..utils import configure_logger, get_app_name
|
|
10
12
|
from . import (
|
|
11
13
|
authenticate,
|
|
12
14
|
process,
|
|
@@ -30,8 +32,8 @@ mapillary_tools_commands = [
|
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
#
|
|
34
|
-
LOG = logging.getLogger(
|
|
35
|
+
# Root logger of mapillary_tools (not including third-party libraries)
|
|
36
|
+
LOG = logging.getLogger(get_app_name())
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
# Handle shared arguments/options here
|
|
@@ -78,13 +80,6 @@ def add_general_arguments(parser, command):
|
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
|
|
81
|
-
def configure_logger(logger: logging.Logger, stream=None) -> None:
|
|
82
|
-
formatter = logging.Formatter("%(asctime)s - %(levelname)-7s - %(message)s")
|
|
83
|
-
handler = logging.StreamHandler(stream)
|
|
84
|
-
handler.setFormatter(formatter)
|
|
85
|
-
logger.addHandler(handler)
|
|
86
|
-
|
|
87
|
-
|
|
88
83
|
def _log_params(argvars: dict) -> None:
|
|
89
84
|
MAX_ENTRIES = 5
|
|
90
85
|
|
|
@@ -151,9 +146,7 @@ def main():
|
|
|
151
146
|
|
|
152
147
|
args = parser.parse_args()
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
configure_logger(LOG, sys.stderr)
|
|
156
|
-
LOG.setLevel(log_level)
|
|
149
|
+
configure_logger(LOG, level=logging.DEBUG if args.verbose else logging.INFO)
|
|
157
150
|
|
|
158
151
|
LOG.debug("%s", version_text)
|
|
159
152
|
argvars = vars(args)
|
|
@@ -162,16 +155,22 @@ def main():
|
|
|
162
155
|
try:
|
|
163
156
|
args.func(argvars)
|
|
164
157
|
except requests.HTTPError as ex:
|
|
165
|
-
|
|
158
|
+
log_exception(ex)
|
|
166
159
|
# TODO: standardize exit codes as exceptions.MapillaryUserError
|
|
167
160
|
sys.exit(16)
|
|
168
161
|
|
|
162
|
+
except api_v4.HTTPContentError as ex:
|
|
163
|
+
log_exception(ex)
|
|
164
|
+
sys.exit(17)
|
|
165
|
+
|
|
169
166
|
except exceptions.MapillaryUserError as ex:
|
|
170
|
-
|
|
171
|
-
"%s: %s", ex.__class__.__name__, ex, exc_info=log_level == logging.DEBUG
|
|
172
|
-
)
|
|
167
|
+
log_exception(ex)
|
|
173
168
|
sys.exit(ex.exit_code)
|
|
174
169
|
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
LOG.info("Interrupted by user...")
|
|
172
|
+
sys.exit(130)
|
|
173
|
+
|
|
175
174
|
|
|
176
175
|
if __name__ == "__main__":
|
|
177
176
|
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from .. import constants
|
|
4
|
+
from ..authenticate import fetch_user_items
|
|
5
|
+
from ..upload import upload
|
|
6
|
+
from .process import bold_text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Command:
|
|
10
|
+
name = "upload"
|
|
11
|
+
help = "Upload processed data to Mapillary"
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def add_common_upload_options(group):
|
|
15
|
+
group.add_argument(
|
|
16
|
+
"--user_name",
|
|
17
|
+
help="The Mapillary user account to upload to.",
|
|
18
|
+
required=False,
|
|
19
|
+
)
|
|
20
|
+
group.add_argument(
|
|
21
|
+
"--organization_key",
|
|
22
|
+
help="The Mapillary organization ID to upload to.",
|
|
23
|
+
default=None,
|
|
24
|
+
required=False,
|
|
25
|
+
)
|
|
26
|
+
group.add_argument(
|
|
27
|
+
"--num_upload_workers",
|
|
28
|
+
help="Number of concurrent upload workers for uploading images. [default: %(default)s]",
|
|
29
|
+
default=constants.MAX_IMAGE_UPLOAD_WORKERS,
|
|
30
|
+
type=int,
|
|
31
|
+
required=False,
|
|
32
|
+
)
|
|
33
|
+
group.add_argument(
|
|
34
|
+
"--reupload",
|
|
35
|
+
help="Re-upload data that has already been uploaded.",
|
|
36
|
+
action="store_true",
|
|
37
|
+
default=False,
|
|
38
|
+
required=False,
|
|
39
|
+
)
|
|
40
|
+
group.add_argument(
|
|
41
|
+
"--dry_run",
|
|
42
|
+
"--dryrun",
|
|
43
|
+
help="[DEVELOPMENT] Simulate upload by sending data to a local directory instead of Mapillary servers. Uses a temporary directory by default unless specified by MAPILLARY_UPLOAD_ENDPOINT environment variable.",
|
|
44
|
+
action="store_true",
|
|
45
|
+
default=False,
|
|
46
|
+
required=False,
|
|
47
|
+
)
|
|
48
|
+
group.add_argument(
|
|
49
|
+
"--nofinish",
|
|
50
|
+
help="[DEVELOPMENT] Upload data without finalizing. The data will NOT be stored permanently or appear on the Mapillary website.",
|
|
51
|
+
action="store_true",
|
|
52
|
+
default=False,
|
|
53
|
+
required=False,
|
|
54
|
+
)
|
|
55
|
+
group.add_argument(
|
|
56
|
+
"--noresume",
|
|
57
|
+
help="[DEVELOPMENT] Start upload from the beginning, ignoring any previously interrupted upload sessions.",
|
|
58
|
+
action="store_true",
|
|
59
|
+
default=False,
|
|
60
|
+
required=False,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def add_basic_arguments(self, parser):
|
|
64
|
+
group = parser.add_argument_group(bold_text("UPLOAD OPTIONS"))
|
|
65
|
+
group.add_argument(
|
|
66
|
+
"--desc_path",
|
|
67
|
+
help=f'Path to the description file with processed image and video metadata (from process command). Use "-" for STDIN. [default: {{IMPORT_PATH}}/{constants.IMAGE_DESCRIPTION_FILENAME}]',
|
|
68
|
+
default=None,
|
|
69
|
+
required=False,
|
|
70
|
+
)
|
|
71
|
+
Command.add_common_upload_options(group)
|
|
72
|
+
|
|
73
|
+
def run(self, vars_args: dict):
|
|
74
|
+
if "user_items" not in vars_args:
|
|
75
|
+
user_items_args = {
|
|
76
|
+
k: v
|
|
77
|
+
for k, v in vars_args.items()
|
|
78
|
+
if k in inspect.getfullargspec(fetch_user_items).args
|
|
79
|
+
}
|
|
80
|
+
vars_args["user_items"] = fetch_user_items(**user_items_args)
|
|
81
|
+
|
|
82
|
+
upload(
|
|
83
|
+
**{
|
|
84
|
+
k: v
|
|
85
|
+
for k, v in vars_args.items()
|
|
86
|
+
if k in inspect.getfullargspec(upload).args
|
|
87
|
+
}
|
|
88
|
+
)
|
|
@@ -6,6 +6,8 @@ import sys
|
|
|
6
6
|
import typing as T
|
|
7
7
|
from typing import TypedDict
|
|
8
8
|
|
|
9
|
+
import jsonschema
|
|
10
|
+
|
|
9
11
|
if sys.version_info >= (3, 11):
|
|
10
12
|
from typing import Required
|
|
11
13
|
else:
|
|
@@ -50,6 +52,9 @@ UserItemSchema = {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
|
|
55
|
+
UserItemSchemaValidator = jsonschema.Draft202012Validator(UserItemSchema)
|
|
56
|
+
|
|
57
|
+
|
|
53
58
|
def _load_config(config_path: str) -> configparser.ConfigParser:
|
|
54
59
|
config = configparser.ConfigParser()
|
|
55
60
|
# Override to not change option names (by default it will lower them)
|