mapillary-tools 0.14.0b1__tar.gz → 0.14.1__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.
Files changed (88) hide show
  1. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/PKG-INFO +7 -6
  2. mapillary_tools-0.14.1/mapillary_tools/__init__.py +1 -0
  3. mapillary_tools-0.14.1/mapillary_tools/api_v4.py +176 -0
  4. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/authenticate.py +46 -38
  5. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/__main__.py +15 -16
  6. mapillary_tools-0.14.1/mapillary_tools/commands/upload.py +88 -0
  7. mapillary_tools-0.14.1/mapillary_tools/constants.py +175 -0
  8. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/exceptions.py +4 -0
  9. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/exif_read.py +2 -1
  10. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/exif_write.py +3 -1
  11. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geo.py +16 -0
  12. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/base.py +6 -2
  13. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/factory.py +9 -1
  14. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_images_from_exiftool.py +1 -1
  15. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -6
  16. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_videos_from_exiftool.py +30 -9
  17. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/utils.py +9 -12
  18. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/video_extractors/gpx.py +2 -1
  19. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/video_extractors/native.py +25 -0
  20. mapillary_tools-0.14.1/mapillary_tools/history.py +182 -0
  21. mapillary_tools-0.14.1/mapillary_tools/http.py +211 -0
  22. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  23. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/process_geotag_properties.py +31 -27
  24. mapillary_tools-0.14.1/mapillary_tools/process_sequence_properties.py +714 -0
  25. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/sample_video.py +1 -2
  26. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/serializer/description.py +56 -56
  27. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/serializer/gpx.py +1 -1
  28. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/upload.py +201 -205
  29. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/upload_api_v4.py +57 -47
  30. mapillary_tools-0.14.1/mapillary_tools/uploader.py +1162 -0
  31. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/utils.py +57 -5
  32. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools.egg-info/PKG-INFO +7 -6
  33. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools.egg-info/SOURCES.txt +1 -0
  34. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools.egg-info/requires.txt +6 -5
  35. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/pyproject.toml +6 -5
  36. mapillary_tools-0.14.0b1/mapillary_tools/__init__.py +0 -1
  37. mapillary_tools-0.14.0b1/mapillary_tools/api_v4.py +0 -373
  38. mapillary_tools-0.14.0b1/mapillary_tools/commands/upload.py +0 -59
  39. mapillary_tools-0.14.0b1/mapillary_tools/constants.py +0 -93
  40. mapillary_tools-0.14.0b1/mapillary_tools/history.py +0 -65
  41. mapillary_tools-0.14.0b1/mapillary_tools/process_sequence_properties.py +0 -697
  42. mapillary_tools-0.14.0b1/mapillary_tools/uploader.py +0 -727
  43. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/LICENSE +0 -0
  44. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/README.md +0 -0
  45. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/blackvue_parser.py +0 -0
  46. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/camm/camm_builder.py +0 -0
  47. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/camm/camm_parser.py +0 -0
  48. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/__init__.py +0 -0
  49. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/authenticate.py +0 -0
  50. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/process.py +0 -0
  51. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/process_and_upload.py +0 -0
  52. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/sample_video.py +0 -0
  53. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/video_process.py +0 -0
  54. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/video_process_and_upload.py +0 -0
  55. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/commands/zip.py +0 -0
  56. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/config.py +0 -0
  57. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/exiftool_read.py +0 -0
  58. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/exiftool_read_video.py +0 -0
  59. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/exiftool_runner.py +0 -0
  60. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/ffmpeg.py +0 -0
  61. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/__init__.py +0 -0
  62. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_images_from_exif.py +0 -0
  63. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -0
  64. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
  65. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
  66. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_videos_from_gpx.py +0 -0
  67. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/geotag_videos_from_video.py +0 -0
  68. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/image_extractors/base.py +0 -0
  69. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/image_extractors/exif.py +0 -0
  70. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/image_extractors/exiftool.py +0 -0
  71. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/options.py +0 -0
  72. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/video_extractors/base.py +0 -0
  73. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/geotag/video_extractors/exiftool.py +0 -0
  74. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/gpmf/gpmf_gps_filter.py +0 -0
  75. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/gpmf/gpmf_parser.py +0 -0
  76. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/gpmf/gps_filter.py +0 -0
  77. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/ipc.py +0 -0
  78. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/mp4/__init__.py +0 -0
  79. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/mp4/io_utils.py +0 -0
  80. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/mp4/mp4_sample_parser.py +0 -0
  81. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
  82. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/mp4/simple_mp4_parser.py +0 -0
  83. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/telemetry.py +0 -0
  84. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools/types.py +0 -0
  85. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools.egg-info/dependency_links.txt +0 -0
  86. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools.egg-info/entry_points.txt +0 -0
  87. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/mapillary_tools.egg-info/top_level.txt +0 -0
  88. {mapillary_tools-0.14.0b1 → mapillary_tools-0.14.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapillary_tools
3
- Version: 0.14.0b1
3
+ Version: 0.14.1
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<3.0.0,>=2.10.0
26
- Requires-Dist: exifread==2.3.2
27
- Requires-Dist: gpxpy<1.6.0,>=1.5.0
28
- Requires-Dist: jsonschema~=4.17.3
29
- Requires-Dist: piexif==1.1.3
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.1"
@@ -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, api_v4._sanitize(user_items)
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, api_v4._sanitize(user_items)
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
- user_items = _verify_user_auth(_validate_profile(user_items))
133
-
134
- LOG.info(
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
- if organization_key is not None:
139
- resp = api_v4.fetch_organization(
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
- LOG.info("Uploading to Mapillary organization: %s", json.dumps(resp.json()))
143
- user_items["MAPOrganizationKey"] = organization_key
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
 
@@ -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
- try:
176
- resp = api_v4.fetch_user_or_me(
177
- user_access_token=user_items["user_upload_token"]
178
- )
179
- except requests.HTTPError as ex:
180
- if api_v4.is_auth_error(ex.response):
181
- message = api_v4.extract_auth_error_message(ex.response)
182
- raise exceptions.MapillaryUploadUnauthorizedError(message)
183
- else:
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
- user_json = resp.json()
193
+ data = api_v4.jsonify_response(resp)
187
194
 
188
195
  return {
189
196
  **user_items,
190
- "MAPSettingsUsername": user_json.get("username"),
191
- "MAPSettingsUserKey": user_json.get("id"),
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
- try:
279
- resp = api_v4.get_upload_token(user_email, user_password)
280
- except requests.HTTPError as ex:
281
- if not _enabled:
282
- raise ex
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
- if _is_login_retryable(ex):
285
- return _prompt_login()
292
+ if _is_login_retryable(ex):
293
+ return _prompt_login()
286
294
 
287
- raise ex
295
+ raise ex
288
296
 
289
- data = resp.json()
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
- # do not use __name__ here is because if you run tools as a module, __name__ will be "__main__"
34
- LOG = logging.getLogger("mapillary_tools")
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
- log_level = logging.DEBUG if args.verbose else logging.INFO
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
- LOG.error("%s: %s", ex.__class__.__name__, api_v4.readable_http_error(ex))
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
- LOG.error(
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
+ )
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import os
5
+ import tempfile
6
+
7
+ import appdirs
8
+
9
+ _ENV_PREFIX = "MAPILLARY_TOOLS_"
10
+
11
+
12
+ def _yes_or_no(val: str) -> bool:
13
+ return val.strip().upper() in ["1", "TRUE", "YES"]
14
+
15
+
16
+ def _parse_scaled_integers(
17
+ value: str, scale: dict[str, int] | None = None
18
+ ) -> int | None:
19
+ """
20
+ >>> scale = {"": 1, "b": 1, "K": 1024, "M": 1024 * 1024, "G": 1024 * 1024 * 1024}
21
+ >>> _parse_scaled_integers("0", scale=scale)
22
+ 0
23
+ >>> _parse_scaled_integers("10", scale=scale)
24
+ 10
25
+ >>> _parse_scaled_integers("100B", scale=scale)
26
+ 100
27
+ >>> _parse_scaled_integers("100k", scale=scale)
28
+ 102400
29
+ >>> _parse_scaled_integers("100t", scale=scale)
30
+ Traceback (most recent call last):
31
+ ValueError: Expect valid integer ends with , b, K, M, G, but got 100T
32
+ """
33
+
34
+ if scale is None:
35
+ scale = {"": 1}
36
+
37
+ value = value.strip().upper()
38
+
39
+ if value in ["INF", "INFINITY"]:
40
+ return None
41
+
42
+ try:
43
+ for k, v in scale.items():
44
+ k = k.upper()
45
+ if k and value.endswith(k):
46
+ return int(value[: -len(k)]) * v
47
+
48
+ if "" in scale:
49
+ return int(value) * scale[""]
50
+ except ValueError:
51
+ pass
52
+
53
+ raise ValueError(
54
+ f"Expect valid integer ends with {', '.join(scale.keys())}, but got {value}"
55
+ )
56
+
57
+
58
+ _parse_pixels = functools.partial(
59
+ _parse_scaled_integers,
60
+ scale={
61
+ "": 1,
62
+ "K": 1000,
63
+ "M": 1000 * 1000,
64
+ "MP": 1000 * 1000,
65
+ "G": 1000 * 1000 * 1000,
66
+ "GP": 1000 * 1000 * 1000,
67
+ },
68
+ )
69
+
70
+ _parse_filesize = functools.partial(
71
+ _parse_scaled_integers,
72
+ scale={"B": 1, "K": 1024, "M": 1024 * 1024, "G": 1024 * 1024 * 1024},
73
+ )
74
+
75
+ ###################
76
+ ##### GENERAL #####
77
+ ###################
78
+ USER_DATA_DIR = appdirs.user_data_dir(appname="mapillary_tools", appauthor="Mapillary")
79
+ PROMPT_DISABLED: bool = _yes_or_no(os.getenv(_ENV_PREFIX + "PROMPT_DISABLED", "NO"))
80
+
81
+
82
+ ############################
83
+ ##### VIDEO PROCESSING #####
84
+ ############################
85
+ # In seconds
86
+ VIDEO_SAMPLE_INTERVAL = float(os.getenv(_ENV_PREFIX + "VIDEO_SAMPLE_INTERVAL", -1))
87
+ # In meters
88
+ VIDEO_SAMPLE_DISTANCE = float(os.getenv(_ENV_PREFIX + "VIDEO_SAMPLE_DISTANCE", 3))
89
+ VIDEO_DURATION_RATIO = float(os.getenv(_ENV_PREFIX + "VIDEO_DURATION_RATIO", 1))
90
+ FFPROBE_PATH: str = os.getenv(_ENV_PREFIX + "FFPROBE_PATH", "ffprobe")
91
+ FFMPEG_PATH: str = os.getenv(_ENV_PREFIX + "FFMPEG_PATH", "ffmpeg")
92
+ EXIFTOOL_PATH: str = os.getenv(_ENV_PREFIX + "EXIFTOOL_PATH", "exiftool")
93
+ IMAGE_DESCRIPTION_FILENAME = os.getenv(
94
+ _ENV_PREFIX + "IMAGE_DESCRIPTION_FILENAME", "mapillary_image_description.json"
95
+ )
96
+ SAMPLED_VIDEO_FRAMES_FILENAME = os.getenv(
97
+ _ENV_PREFIX + "SAMPLED_VIDEO_FRAMES_FILENAME", "mapillary_sampled_video_frames"
98
+ )
99
+ # DoP value, the lower the better
100
+ # See https://github.com/gopro/gpmf-parser#hero5-black-with-gps-enabled-adds
101
+ # It is used to filter out noisy points
102
+ GOPRO_MAX_DOP100 = int(os.getenv(_ENV_PREFIX + "GOPRO_MAX_DOP100", 1000))
103
+ # Within the GPS stream: 0 - no lock, 2 or 3 - 2D or 3D Lock
104
+ GOPRO_GPS_FIXES: set[int] = set(
105
+ int(fix) for fix in os.getenv(_ENV_PREFIX + "GOPRO_GPS_FIXES", "2,3").split(",")
106
+ )
107
+ # GPS precision, in meters, is used to filter outliers
108
+ GOPRO_GPS_PRECISION = float(os.getenv(_ENV_PREFIX + "GOPRO_GPS_PRECISION", 15))
109
+ MAPILLARY__EXPERIMENTAL_ENABLE_IMU: bool = _yes_or_no(
110
+ os.getenv("MAPILLARY__EXPERIMENTAL_ENABLE_IMU", "NO")
111
+ )
112
+
113
+
114
+ #################################
115
+ ###### SEQUENCE PROCESSING ######
116
+ #################################
117
+ # In meters
118
+ CUTOFF_DISTANCE = float(os.getenv(_ENV_PREFIX + "CUTOFF_DISTANCE", 600))
119
+ # In seconds
120
+ CUTOFF_TIME = float(os.getenv(_ENV_PREFIX + "CUTOFF_TIME", 60))
121
+ DUPLICATE_DISTANCE = float(os.getenv(_ENV_PREFIX + "DUPLICATE_DISTANCE", 0.1))
122
+ DUPLICATE_ANGLE = float(os.getenv(_ENV_PREFIX + "DUPLICATE_ANGLE", 5))
123
+ MAX_CAPTURE_SPEED_KMH = float(
124
+ os.getenv(_ENV_PREFIX + "MAX_CAPTURE_SPEED_KMH", 400)
125
+ ) # 400 KM/h
126
+ # WARNING: Changing the following envvars might result in failed uploads
127
+ # Max number of images per sequence
128
+ MAX_SEQUENCE_LENGTH: int | None = _parse_scaled_integers(
129
+ os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_LENGTH", "1000")
130
+ )
131
+ # Max file size per sequence (sum of image filesizes in the sequence)
132
+ MAX_SEQUENCE_FILESIZE: int | None = _parse_filesize(
133
+ os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_FILESIZE", "110G")
134
+ )
135
+ # Max number of pixels per sequence (sum of image pixels in the sequence)
136
+ MAX_SEQUENCE_PIXELS: int | None = _parse_pixels(
137
+ os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_PIXELS", "6G")
138
+ )
139
+
140
+
141
+ ##################
142
+ ##### UPLOAD #####
143
+ ##################
144
+ MAPILLARY_DISABLE_API_LOGGING: bool = _yes_or_no(
145
+ os.getenv("MAPILLARY_DISABLE_API_LOGGING", "NO")
146
+ )
147
+ MAPILLARY_UPLOAD_HISTORY_PATH: str = os.getenv(
148
+ "MAPILLARY_UPLOAD_HISTORY_PATH", os.path.join(USER_DATA_DIR, "upload_history")
149
+ )
150
+ UPLOAD_CACHE_DIR: str = os.getenv(
151
+ _ENV_PREFIX + "UPLOAD_CACHE_DIR",
152
+ os.path.join(tempfile.gettempdir(), "mapillary_tools", "upload_cache"),
153
+ )
154
+ # The minimal upload speed is used to calculate the read timeout to avoid upload hanging:
155
+ # timeout = upload_size / MIN_UPLOAD_SPEED
156
+ MIN_UPLOAD_SPEED: int | None = _parse_filesize(
157
+ os.getenv(_ENV_PREFIX + "MIN_UPLOAD_SPEED", "50K") # 50 Kb/s
158
+ )
159
+ # Maximum number of parallel workers for uploading images within a single sequence.
160
+ # NOTE: Sequences themselves are uploaded sequentially, not in parallel.
161
+ MAX_IMAGE_UPLOAD_WORKERS: int = int(
162
+ os.getenv(_ENV_PREFIX + "MAX_IMAGE_UPLOAD_WORKERS", 4)
163
+ )
164
+ # The chunk size in MB (see chunked transfer encoding https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
165
+ # for uploading data to MLY upload service.
166
+ # Changing this size does not change the number of requests nor affect upload performance,
167
+ # but it affects the responsiveness of the upload progress bar
168
+ UPLOAD_CHUNK_SIZE_MB: float = float(os.getenv(_ENV_PREFIX + "UPLOAD_CHUNK_SIZE_MB", 2))
169
+ MAX_UPLOAD_RETRIES: int = int(os.getenv(_ENV_PREFIX + "MAX_UPLOAD_RETRIES", 200))
170
+ MAPILLARY__ENABLE_UPLOAD_HISTORY_FOR_DRY_RUN: bool = _yes_or_no(
171
+ os.getenv("MAPILLARY__ENABLE_UPLOAD_HISTORY_FOR_DRY_RUN", "NO")
172
+ )
173
+ _AUTH_VERIFICATION_DISABLED: bool = _yes_or_no(
174
+ os.getenv(_ENV_PREFIX + "_AUTH_VERIFICATION_DISABLED", "NO")
175
+ )
@@ -51,6 +51,10 @@ class MapillaryVideoGPSNotFoundError(MapillaryDescriptionError):
51
51
  pass
52
52
 
53
53
 
54
+ class MapillaryInvalidVideoError(MapillaryDescriptionError):
55
+ pass
56
+
57
+
54
58
  class MapillaryGPXEmptyError(MapillaryDescriptionError):
55
59
  pass
56
60