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.
Files changed (76) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +5 -4
  3. mapillary_tools/authenticate.py +9 -9
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/camm/camm_parser.py +5 -5
  6. mapillary_tools/commands/__main__.py +1 -2
  7. mapillary_tools/config.py +41 -18
  8. mapillary_tools/constants.py +3 -2
  9. mapillary_tools/exceptions.py +1 -1
  10. mapillary_tools/exif_read.py +65 -65
  11. mapillary_tools/exif_write.py +7 -7
  12. mapillary_tools/exiftool_read.py +23 -46
  13. mapillary_tools/exiftool_read_video.py +88 -49
  14. mapillary_tools/exiftool_runner.py +4 -24
  15. mapillary_tools/ffmpeg.py +417 -242
  16. mapillary_tools/geo.py +4 -21
  17. mapillary_tools/geotag/__init__.py +0 -1
  18. mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
  19. mapillary_tools/geotag/factory.py +105 -103
  20. mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
  21. mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
  22. mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
  23. mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
  24. mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
  25. mapillary_tools/geotag/geotag_images_from_video.py +51 -14
  26. mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
  27. mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
  28. mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
  29. mapillary_tools/geotag/image_extractors/base.py +18 -0
  30. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  31. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  32. mapillary_tools/geotag/options.py +26 -3
  33. mapillary_tools/geotag/utils.py +62 -0
  34. mapillary_tools/geotag/video_extractors/base.py +18 -0
  35. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  36. mapillary_tools/geotag/video_extractors/gpx.py +116 -0
  37. mapillary_tools/geotag/video_extractors/native.py +135 -0
  38. mapillary_tools/gpmf/gpmf_parser.py +16 -16
  39. mapillary_tools/gpmf/gps_filter.py +5 -3
  40. mapillary_tools/history.py +8 -3
  41. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  42. mapillary_tools/mp4/mp4_sample_parser.py +27 -27
  43. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  44. mapillary_tools/mp4/simple_mp4_parser.py +13 -12
  45. mapillary_tools/process_geotag_properties.py +21 -15
  46. mapillary_tools/process_sequence_properties.py +49 -49
  47. mapillary_tools/sample_video.py +15 -14
  48. mapillary_tools/serializer/description.py +587 -0
  49. mapillary_tools/serializer/gpx.py +132 -0
  50. mapillary_tools/telemetry.py +6 -5
  51. mapillary_tools/types.py +64 -635
  52. mapillary_tools/upload.py +176 -197
  53. mapillary_tools/upload_api_v4.py +94 -51
  54. mapillary_tools/uploader.py +284 -138
  55. mapillary_tools/utils.py +16 -18
  56. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
  57. mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
  58. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
  59. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
  60. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
  61. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  62. mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
  63. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  64. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
  65. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
  66. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
  67. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
  68. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  69. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
  70. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
  71. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  72. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  73. mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
  74. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
  75. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
  76. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.14.0a1"
1
+ VERSION = "0.14.0b1"
mapillary_tools/api_v4.py CHANGED
@@ -25,6 +25,7 @@ class ClusterFileType(enum.Enum):
25
25
  ZIP = "zip"
26
26
  BLACKVUE = "mly_blackvue_video"
27
27
  CAMM = "mly_camm_video"
28
+ MLY_BUNDLE_MANIFEST = "mly_bundle_manifest"
28
29
 
29
30
 
30
31
  class HTTPSystemCertsAdapter(HTTPAdapter):
@@ -135,7 +136,7 @@ def _log_debug_response(resp: requests.Response):
135
136
  if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
136
137
  return
137
138
 
138
- data: T.Union[str, bytes]
139
+ data: str | bytes
139
140
  try:
140
141
  data = _truncate(dumps(_sanitize(resp.json())))
141
142
  except Exception:
@@ -148,7 +149,7 @@ def readable_http_error(ex: requests.HTTPError) -> str:
148
149
  req = ex.request
149
150
  resp = ex.response
150
151
 
151
- data: T.Union[str, bytes]
152
+ data: str | bytes
152
153
  try:
153
154
  data = _truncate(dumps(_sanitize(resp.json())))
154
155
  except Exception:
@@ -284,7 +285,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
284
285
 
285
286
 
286
287
  def fetch_organization(
287
- user_access_token: str, organization_id: T.Union[int, str]
288
+ user_access_token: str, organization_id: int | str
288
289
  ) -> requests.Response:
289
290
  resp = request_get(
290
291
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
@@ -329,7 +330,7 @@ ActionType = T.Literal[
329
330
  ]
330
331
 
331
332
 
332
- def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
333
+ def log_event(action_type: ActionType, properties: dict) -> requests.Response:
333
334
  resp = request_post(
334
335
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
335
336
  json={
@@ -11,7 +11,7 @@ import jsonschema
11
11
 
12
12
  import requests
13
13
 
14
- from . import api_v4, config, constants, exceptions, types
14
+ from . import api_v4, config, constants, exceptions
15
15
 
16
16
 
17
17
  LOG = logging.getLogger(__name__)
@@ -64,7 +64,7 @@ def authenticate(
64
64
  LOG.info('Creating new profile: "%s"', profile_name)
65
65
 
66
66
  if jwt:
67
- user_items: types.UserItem = {"user_upload_token": jwt}
67
+ user_items: config.UserItem = {"user_upload_token": jwt}
68
68
  user_items = _verify_user_auth(_validate_profile(user_items))
69
69
  else:
70
70
  user_items = _prompt_login(
@@ -89,7 +89,7 @@ def authenticate(
89
89
  def fetch_user_items(
90
90
  user_name: str | None = None,
91
91
  organization_key: str | None = None,
92
- ) -> types.UserItem:
92
+ ) -> config.UserItem:
93
93
  """
94
94
  Read user information from the config file,
95
95
  or prompt the user to authenticate if the specified profile does not exist
@@ -155,9 +155,9 @@ def _prompt(message: str) -> str:
155
155
  return input()
156
156
 
157
157
 
158
- def _validate_profile(user_items: types.UserItem) -> types.UserItem:
158
+ def _validate_profile(user_items: config.UserItem) -> config.UserItem:
159
159
  try:
160
- jsonschema.validate(user_items, types.UserItemSchema)
160
+ jsonschema.validate(user_items, config.UserItemSchema)
161
161
  except jsonschema.ValidationError as ex:
162
162
  raise exceptions.MapillaryBadParameterError(
163
163
  f"Invalid profile format: {ex.message}"
@@ -165,7 +165,7 @@ def _validate_profile(user_items: types.UserItem) -> types.UserItem:
165
165
  return user_items
166
166
 
167
167
 
168
- def _verify_user_auth(user_items: types.UserItem) -> types.UserItem:
168
+ def _verify_user_auth(user_items: config.UserItem) -> config.UserItem:
169
169
  """
170
170
  Verify that the user access token is valid
171
171
  """
@@ -205,7 +205,7 @@ def _validate_profile_name(profile_name: str):
205
205
  )
206
206
 
207
207
 
208
- def _list_all_profiles(profiles: dict[str, types.UserItem]) -> None:
208
+ def _list_all_profiles(profiles: dict[str, config.UserItem]) -> None:
209
209
  _echo("Existing Mapillary profiles:")
210
210
 
211
211
  # Header
@@ -256,7 +256,7 @@ def _is_login_retryable(ex: requests.HTTPError) -> bool:
256
256
  def _prompt_login(
257
257
  user_email: str | None = None,
258
258
  user_password: str | None = None,
259
- ) -> types.UserItem:
259
+ ) -> config.UserItem:
260
260
  _enabled = _prompt_enabled()
261
261
 
262
262
  if user_email is None:
@@ -288,7 +288,7 @@ def _prompt_login(
288
288
 
289
289
  data = resp.json()
290
290
 
291
- user_items: types.UserItem = {
291
+ user_items: config.UserItem = {
292
292
  "user_upload_token": str(data["access_token"]),
293
293
  "MAPSettingsUserKey": str(data["user_id"]),
294
294
  }
@@ -14,15 +14,14 @@ from .mp4 import simple_mp4_parser as sparser
14
14
 
15
15
 
16
16
  LOG = logging.getLogger(__name__)
17
- # An example: [1623057074211]$GPVTG,,T,,M,0.078,N,0.144,K,D*28[1623057075215]
18
17
  NMEA_LINE_REGEX = re.compile(
19
18
  rb"""
20
19
  ^\s*
21
- \[(\d+)\] # timestamp
20
+ \[(\d+)\] # Timestamp
22
21
  \s*
23
- (\$\w{5}.*) # nmea line
22
+ (\$\w{5}.*) # NMEA message
24
23
  \s*
25
- (\[\d+\])? # strange timestamp
24
+ (\[\d+\])? # Strange timestamp
26
25
  \s*$
27
26
  """,
28
27
  re.X,
@@ -47,7 +46,7 @@ def extract_blackvue_info(fp: T.BinaryIO) -> BlackVueInfo | None:
47
46
  if gps_data is None:
48
47
  return None
49
48
 
50
- points = list(_parse_gps_box(gps_data))
49
+ points = _parse_gps_box(gps_data)
51
50
  points.sort(key=lambda p: p.time)
52
51
 
53
52
  if points:
@@ -83,8 +82,12 @@ def extract_camera_model(fp: T.BinaryIO) -> str:
83
82
 
84
83
 
85
84
  def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str:
86
- # examples: b' {"model":"DR900X Plus","ver":0.918,"lang":"English","direct":1,"psn":"","temp":34,"GPS":1}\x00'
87
- # b' Pittasoft Co., Ltd.;DR900S-1CH;1.008;English;1;D90SS1HAE00661;T69;\x00'
85
+ """
86
+ >>> _extract_camera_model_from_cprt(b' {"model":"DR900X Plus","ver":0.918,"lang":"English","direct":1,"psn":"","temp":34,"GPS":1}')
87
+ 'DR900X Plus'
88
+ >>> _extract_camera_model_from_cprt(b' Pittasoft Co., Ltd.;DR900S-1CH;1.008;English;1;D90SS1HAE00661;T69;')
89
+ 'DR900S-1CH'
90
+ """
88
91
  cprt_bytes = cprt_bytes.strip().strip(b"\x00")
89
92
 
90
93
  try:
@@ -111,28 +114,82 @@ def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str:
111
114
  return ""
112
115
 
113
116
 
114
- def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]:
117
+ def _parse_gps_box(gps_data: bytes) -> list[geo.Point]:
118
+ """
119
+ >>> list(_parse_gps_box(b"[1623057074211]$GPGGA,202530.00,5109.0262,N,11401.8407,W,5,40,0.5,1097.36,M,-17.00,M,18,TSTR*61"))
120
+ [Point(time=1623057074211, lat=51.150436666666664, lon=-114.03067833333333, alt=1097.36, angle=None)]
121
+
122
+ >>> list(_parse_gps_box(b"[1629874404069]$GNGGA,175322.00,3244.53126,N,11710.97811,W,1,12,0.84,17.4,M,-34.0,M,,*45"))
123
+ [Point(time=1629874404069, lat=32.742187666666666, lon=-117.1829685, alt=17.4, angle=None)]
124
+
125
+ >>> list(_parse_gps_box(b"[1629874404069]$GNGLL,4404.14012,N,12118.85993,W,001037.00,A,A*67"))
126
+ [Point(time=1629874404069, lat=44.069002, lon=-121.31433216666667, alt=None, angle=None)]
127
+
128
+ >>> list(_parse_gps_box(b"[1629874404069]$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B"))
129
+ [Point(time=1629874404069, lat=44.06899883333333, lon=-121.31433716666666, alt=None, angle=None)]
130
+
131
+ >>> list(_parse_gps_box(b"[1623057074211]$GPVTG,,T,,M,0.078,N,0.144,K,D*28[1623057075215]"))
132
+ []
133
+ """
134
+ points_by_sentence_type: dict[str, list[geo.Point]] = {}
135
+
115
136
  for line_bytes in gps_data.splitlines():
116
137
  match = NMEA_LINE_REGEX.match(line_bytes)
117
138
  if match is None:
118
139
  continue
119
140
  nmea_line_bytes = match.group(2)
120
- if nmea_line_bytes.startswith(b"$GPGGA"):
121
- try:
122
- nmea_line = nmea_line_bytes.decode("utf8")
123
- except UnicodeDecodeError:
124
- continue
125
- try:
126
- nmea = pynmea2.parse(nmea_line)
127
- except pynmea2.nmea.ParseError:
141
+
142
+ if not nmea_line_bytes:
143
+ continue
144
+
145
+ try:
146
+ nmea_line = nmea_line_bytes.decode("utf8")
147
+ except UnicodeDecodeError:
148
+ continue
149
+
150
+ if not nmea_line:
151
+ continue
152
+
153
+ try:
154
+ message = pynmea2.parse(nmea_line)
155
+ except pynmea2.nmea.ParseError:
156
+ continue
157
+
158
+ epoch_ms = int(match.group(1))
159
+
160
+ # https://tavotech.com/gps-nmea-sentence-structure/
161
+ if message.sentence_type in ["GGA"]:
162
+ if not message.is_valid:
128
163
  continue
129
- if not nmea.is_valid:
164
+ point = geo.Point(
165
+ time=epoch_ms,
166
+ lat=message.latitude,
167
+ lon=message.longitude,
168
+ alt=message.altitude,
169
+ angle=None,
170
+ )
171
+ points_by_sentence_type.setdefault(message.sentence_type, []).append(point)
172
+
173
+ elif message.sentence_type in ["RMC", "GLL"]:
174
+ if not message.is_valid:
130
175
  continue
131
- epoch_ms = int(match.group(1))
132
- yield geo.Point(
176
+ point = geo.Point(
133
177
  time=epoch_ms,
134
- lat=nmea.latitude,
135
- lon=nmea.longitude,
136
- alt=nmea.altitude,
178
+ lat=message.latitude,
179
+ lon=message.longitude,
180
+ alt=None,
137
181
  angle=None,
138
182
  )
183
+ points_by_sentence_type.setdefault(message.sentence_type, []).append(point)
184
+
185
+ # This is the extraction order in exiftool
186
+ if "RMC" in points_by_sentence_type:
187
+ return points_by_sentence_type["RMC"]
188
+
189
+ if "GGA" in points_by_sentence_type:
190
+ return points_by_sentence_type["GGA"]
191
+
192
+ if "GLL" in points_by_sentence_type:
193
+ return points_by_sentence_type["GLL"]
194
+
195
+ return []
@@ -373,7 +373,7 @@ SAMPLE_ENTRY_CLS_BY_CAMM_TYPE = {
373
373
  assert len(SAMPLE_ENTRY_CLS_BY_CAMM_TYPE) == 5, SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.keys()
374
374
 
375
375
 
376
- _SWITCH: T.Dict[int, C.Struct] = {
376
+ _SWITCH: dict[int, C.Struct] = {
377
377
  # Angle_axis
378
378
  CAMMType.ANGLE_AXIS.value: _Float[3], # type: ignore
379
379
  # Exposure time
@@ -436,7 +436,7 @@ def _parse_telemetry_from_sample(
436
436
 
437
437
  def _filter_telemetry_by_elst_segments(
438
438
  measurements: T.Iterable[TelemetryMeasurement],
439
- elst: T.Sequence[T.Tuple[float, float]],
439
+ elst: T.Sequence[tuple[float, float]],
440
440
  ) -> T.Generator[TelemetryMeasurement, None, None]:
441
441
  empty_elst = [entry for entry in elst if entry[0] == -1]
442
442
  if empty_elst:
@@ -466,8 +466,8 @@ def _filter_telemetry_by_elst_segments(
466
466
 
467
467
 
468
468
  def elst_entry_to_seconds(
469
- entry: T.Dict, movie_timescale: int, media_timescale: int
470
- ) -> T.Tuple[float, float]:
469
+ entry: dict, movie_timescale: int, media_timescale: int
470
+ ) -> tuple[float, float]:
471
471
  assert movie_timescale > 0, "expected positive movie_timescale"
472
472
  assert media_timescale > 0, "expected positive media_timescale"
473
473
  media_time, duration = entry["media_time"], entry["segment_duration"]
@@ -477,7 +477,7 @@ def elst_entry_to_seconds(
477
477
  return (media_time, duration)
478
478
 
479
479
 
480
- def _is_camm_description(description: T.Dict) -> bool:
480
+ def _is_camm_description(description: dict) -> bool:
481
481
  return description["format"] == b"camm"
482
482
 
483
483
 
@@ -2,7 +2,6 @@ import argparse
2
2
  import enum
3
3
  import logging
4
4
  import sys
5
- import typing as T
6
5
  from pathlib import Path
7
6
 
8
7
  import requests
@@ -86,7 +85,7 @@ def configure_logger(logger: logging.Logger, stream=None) -> None:
86
85
  logger.addHandler(handler)
87
86
 
88
87
 
89
- def _log_params(argvars: T.Dict) -> None:
88
+ def _log_params(argvars: dict) -> None:
90
89
  MAX_ENTRIES = 5
91
90
 
92
91
  def _stringify(x) -> str:
mapillary_tools/config.py CHANGED
@@ -1,30 +1,55 @@
1
+ from __future__ import annotations
2
+
1
3
  import configparser
2
4
  import os
5
+ import sys
3
6
  import typing as T
7
+ from typing import TypedDict
4
8
 
5
- from . import api_v4, types
6
-
9
+ if sys.version_info >= (3, 11):
10
+ from typing import Required
11
+ else:
12
+ from typing_extensions import Required
7
13
 
8
- _CLIENT_ID = api_v4.MAPILLARY_CLIENT_TOKEN
9
- # Windows is not happy with | so we convert MLY|ID|TOKEN to MLY_ID_TOKEN
10
- _CLIENT_ID = _CLIENT_ID.replace("|", "_", 2)
14
+ from . import api_v4
11
15
 
12
- DEFAULT_MAPILLARY_FOLDER = os.path.join(
13
- os.path.expanduser("~"),
14
- ".config",
15
- "mapillary",
16
- )
17
16
 
17
+ DEFAULT_MAPILLARY_FOLDER = os.path.join(os.path.expanduser("~"), ".config", "mapillary")
18
18
  MAPILLARY_CONFIG_PATH = os.getenv(
19
19
  "MAPILLARY_CONFIG_PATH",
20
20
  os.path.join(
21
21
  DEFAULT_MAPILLARY_FOLDER,
22
22
  "configs",
23
- _CLIENT_ID,
23
+ # Windows is not happy with | so we convert MLY|ID|TOKEN to MLY_ID_TOKEN
24
+ api_v4.MAPILLARY_CLIENT_TOKEN.replace("|", "_"),
24
25
  ),
25
26
  )
26
27
 
27
28
 
29
+ class UserItem(TypedDict, total=False):
30
+ MAPOrganizationKey: int | str
31
+ # Username
32
+ MAPSettingsUsername: str
33
+ # User ID
34
+ MAPSettingsUserKey: str
35
+ # User access token
36
+ user_upload_token: Required[str]
37
+
38
+
39
+ UserItemSchema = {
40
+ "type": "object",
41
+ "properties": {
42
+ "MAPOrganizationKey": {"type": ["integer", "string"]},
43
+ # Not in use. Keep here for back-compatibility
44
+ "MAPSettingsUsername": {"type": "string"},
45
+ "MAPSettingsUserKey": {"type": "string"},
46
+ "user_upload_token": {"type": "string"},
47
+ },
48
+ "required": ["user_upload_token"],
49
+ "additionalProperties": True,
50
+ }
51
+
52
+
28
53
  def _load_config(config_path: str) -> configparser.ConfigParser:
29
54
  config = configparser.ConfigParser()
30
55
  # Override to not change option names (by default it will lower them)
@@ -34,19 +59,17 @@ def _load_config(config_path: str) -> configparser.ConfigParser:
34
59
  return config
35
60
 
36
61
 
37
- def load_user(
38
- profile_name: str, config_path: T.Optional[str] = None
39
- ) -> T.Optional[types.UserItem]:
62
+ def load_user(profile_name: str, config_path: str | None = None) -> UserItem | None:
40
63
  if config_path is None:
41
64
  config_path = MAPILLARY_CONFIG_PATH
42
65
  config = _load_config(config_path)
43
66
  if not config.has_section(profile_name):
44
67
  return None
45
68
  user_items = dict(config.items(profile_name))
46
- return T.cast(types.UserItem, user_items)
69
+ return T.cast(UserItem, user_items)
47
70
 
48
71
 
49
- def list_all_users(config_path: T.Optional[str] = None) -> T.Dict[str, types.UserItem]:
72
+ def list_all_users(config_path: str | None = None) -> dict[str, UserItem]:
50
73
  if config_path is None:
51
74
  config_path = MAPILLARY_CONFIG_PATH
52
75
  cp = _load_config(config_path)
@@ -58,7 +81,7 @@ def list_all_users(config_path: T.Optional[str] = None) -> T.Dict[str, types.Use
58
81
 
59
82
 
60
83
  def update_config(
61
- profile_name: str, user_items: types.UserItem, config_path: T.Optional[str] = None
84
+ profile_name: str, user_items: UserItem, config_path: str | None = None
62
85
  ) -> None:
63
86
  if config_path is None:
64
87
  config_path = MAPILLARY_CONFIG_PATH
@@ -72,7 +95,7 @@ def update_config(
72
95
  config.write(fp)
73
96
 
74
97
 
75
- def remove_config(profile_name: str, config_path: T.Optional[str] = None) -> None:
98
+ def remove_config(profile_name: str, config_path: str | None = None) -> None:
76
99
  if config_path is None:
77
100
  config_path = MAPILLARY_CONFIG_PATH
78
101
 
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import typing as T
5
4
 
6
5
  import appdirs
7
6
 
@@ -52,7 +51,7 @@ UPLOAD_CHUNK_SIZE_MB = float(os.getenv(_ENV_PREFIX + "UPLOAD_CHUNK_SIZE_MB", 1))
52
51
  # It is used to filter out noisy points
53
52
  GOPRO_MAX_DOP100 = int(os.getenv(_ENV_PREFIX + "GOPRO_MAX_DOP100", 1000))
54
53
  # Within the GPS stream: 0 - no lock, 2 or 3 - 2D or 3D Lock
55
- GOPRO_GPS_FIXES: T.Set[int] = set(
54
+ GOPRO_GPS_FIXES: set[int] = set(
56
55
  int(fix) for fix in os.getenv(_ENV_PREFIX + "GOPRO_GPS_FIXES", "2,3").split(",")
57
56
  )
58
57
  MAX_UPLOAD_RETRIES: int = int(os.getenv(_ENV_PREFIX + "MAX_UPLOAD_RETRIES", 200))
@@ -90,3 +89,5 @@ MAPILLARY_UPLOAD_HISTORY_PATH: str = os.getenv(
90
89
  "upload_history",
91
90
  ),
92
91
  )
92
+
93
+ MAX_IMAGE_UPLOAD_WORKERS = int(os.getenv(_ENV_PREFIX + "MAX_IMAGE_UPLOAD_WORKERS", 64))
@@ -87,7 +87,7 @@ class MapillaryDuplicationError(MapillaryDescriptionError):
87
87
  self.angle_diff = angle_diff
88
88
 
89
89
 
90
- class MapillaryEXIFNotFoundError(MapillaryDescriptionError):
90
+ class MapillaryExifToolXMLNotFoundError(MapillaryDescriptionError):
91
91
  pass
92
92
 
93
93