mapillary-tools 0.14.0a1__py3-none-any.whl → 0.14.0a2__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 (67) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +4 -4
  3. mapillary_tools/camm/camm_parser.py +5 -5
  4. mapillary_tools/commands/__main__.py +1 -2
  5. mapillary_tools/config.py +7 -5
  6. mapillary_tools/constants.py +1 -2
  7. mapillary_tools/exceptions.py +1 -1
  8. mapillary_tools/exif_read.py +65 -65
  9. mapillary_tools/exif_write.py +7 -7
  10. mapillary_tools/exiftool_read.py +23 -46
  11. mapillary_tools/exiftool_read_video.py +36 -34
  12. mapillary_tools/ffmpeg.py +24 -23
  13. mapillary_tools/geo.py +4 -21
  14. mapillary_tools/geotag/{geotag_from_generic.py → base.py} +32 -48
  15. mapillary_tools/geotag/factory.py +27 -34
  16. mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
  17. mapillary_tools/geotag/geotag_images_from_exiftool.py +107 -59
  18. mapillary_tools/geotag/geotag_images_from_gpx.py +20 -10
  19. mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
  20. mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
  21. mapillary_tools/geotag/geotag_images_from_video.py +16 -14
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +14 -115
  24. mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
  25. mapillary_tools/geotag/image_extractors/base.py +18 -0
  26. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  27. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  28. mapillary_tools/geotag/options.py +1 -0
  29. mapillary_tools/geotag/utils.py +62 -0
  30. mapillary_tools/geotag/video_extractors/base.py +18 -0
  31. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  32. mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
  33. mapillary_tools/geotag/video_extractors/native.py +157 -0
  34. mapillary_tools/gpmf/gpmf_parser.py +16 -16
  35. mapillary_tools/gpmf/gps_filter.py +5 -3
  36. mapillary_tools/history.py +4 -2
  37. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  38. mapillary_tools/mp4/mp4_sample_parser.py +27 -27
  39. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  40. mapillary_tools/mp4/simple_mp4_parser.py +13 -12
  41. mapillary_tools/process_geotag_properties.py +5 -7
  42. mapillary_tools/process_sequence_properties.py +40 -38
  43. mapillary_tools/sample_video.py +8 -8
  44. mapillary_tools/telemetry.py +6 -5
  45. mapillary_tools/types.py +33 -38
  46. mapillary_tools/utils.py +16 -18
  47. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +1 -1
  48. mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
  49. mapillary_tools/geotag/__init__.py +0 -1
  50. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
  51. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
  52. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  53. mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
  54. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  55. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
  56. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
  57. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
  58. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
  59. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  60. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
  61. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  62. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  63. mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
  64. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +0 -0
  65. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
  66. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/licenses/LICENSE +0 -0
  67. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.14.0a1"
1
+ VERSION = "0.14.0a2"
mapillary_tools/api_v4.py CHANGED
@@ -135,7 +135,7 @@ def _log_debug_response(resp: requests.Response):
135
135
  if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
136
136
  return
137
137
 
138
- data: T.Union[str, bytes]
138
+ data: str | bytes
139
139
  try:
140
140
  data = _truncate(dumps(_sanitize(resp.json())))
141
141
  except Exception:
@@ -148,7 +148,7 @@ def readable_http_error(ex: requests.HTTPError) -> str:
148
148
  req = ex.request
149
149
  resp = ex.response
150
150
 
151
- data: T.Union[str, bytes]
151
+ data: str | bytes
152
152
  try:
153
153
  data = _truncate(dumps(_sanitize(resp.json())))
154
154
  except Exception:
@@ -284,7 +284,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
284
284
 
285
285
 
286
286
  def fetch_organization(
287
- user_access_token: str, organization_id: T.Union[int, str]
287
+ user_access_token: str, organization_id: int | str
288
288
  ) -> requests.Response:
289
289
  resp = request_get(
290
290
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
@@ -329,7 +329,7 @@ ActionType = T.Literal[
329
329
  ]
330
330
 
331
331
 
332
- def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
332
+ def log_event(action_type: ActionType, properties: dict) -> requests.Response:
333
333
  resp = request_post(
334
334
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
335
335
  json={
@@ -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,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import configparser
2
4
  import os
3
5
  import typing as T
@@ -35,8 +37,8 @@ def _load_config(config_path: str) -> configparser.ConfigParser:
35
37
 
36
38
 
37
39
  def load_user(
38
- profile_name: str, config_path: T.Optional[str] = None
39
- ) -> T.Optional[types.UserItem]:
40
+ profile_name: str, config_path: str | None = None
41
+ ) -> types.UserItem | None:
40
42
  if config_path is None:
41
43
  config_path = MAPILLARY_CONFIG_PATH
42
44
  config = _load_config(config_path)
@@ -46,7 +48,7 @@ def load_user(
46
48
  return T.cast(types.UserItem, user_items)
47
49
 
48
50
 
49
- def list_all_users(config_path: T.Optional[str] = None) -> T.Dict[str, types.UserItem]:
51
+ def list_all_users(config_path: str | None = None) -> dict[str, types.UserItem]:
50
52
  if config_path is None:
51
53
  config_path = MAPILLARY_CONFIG_PATH
52
54
  cp = _load_config(config_path)
@@ -58,7 +60,7 @@ def list_all_users(config_path: T.Optional[str] = None) -> T.Dict[str, types.Use
58
60
 
59
61
 
60
62
  def update_config(
61
- profile_name: str, user_items: types.UserItem, config_path: T.Optional[str] = None
63
+ profile_name: str, user_items: types.UserItem, config_path: str | None = None
62
64
  ) -> None:
63
65
  if config_path is None:
64
66
  config_path = MAPILLARY_CONFIG_PATH
@@ -72,7 +74,7 @@ def update_config(
72
74
  config.write(fp)
73
75
 
74
76
 
75
- def remove_config(profile_name: str, config_path: T.Optional[str] = None) -> None:
77
+ def remove_config(profile_name: str, config_path: str | None = None) -> None:
76
78
  if config_path is None:
77
79
  config_path = MAPILLARY_CONFIG_PATH
78
80
 
@@ -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))
@@ -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
 
@@ -36,7 +36,7 @@ def eval_frac(value: Ratio) -> float:
36
36
  return float(value.num) / float(value.den)
37
37
 
38
38
 
39
- def gps_to_decimal(values: T.Tuple[Ratio, Ratio, Ratio]) -> T.Optional[float]:
39
+ def gps_to_decimal(values: tuple[Ratio, Ratio, Ratio]) -> float | None:
40
40
  try:
41
41
  deg, min, sec, *_ = values
42
42
  except (TypeError, ValueError):
@@ -56,14 +56,14 @@ def gps_to_decimal(values: T.Tuple[Ratio, Ratio, Ratio]) -> T.Optional[float]:
56
56
  return degrees + minutes / 60 + seconds / 3600
57
57
 
58
58
 
59
- def _parse_coord_numeric(coord: str, ref: T.Optional[str]) -> T.Optional[float]:
59
+ def _parse_coord_numeric(coord: str, ref: str | None) -> float | None:
60
60
  try:
61
61
  return float(coord) * SIGN_BY_DIRECTION[ref]
62
62
  except (ValueError, KeyError):
63
63
  return None
64
64
 
65
65
 
66
- def _parse_coord_adobe(coord: str) -> T.Optional[float]:
66
+ def _parse_coord_adobe(coord: str) -> float | None:
67
67
  """
68
68
  Parse Adobe coordinate format: <degrees,fractionalminutes[NSEW]>
69
69
  """
@@ -79,7 +79,7 @@ def _parse_coord_adobe(coord: str) -> T.Optional[float]:
79
79
  return None
80
80
 
81
81
 
82
- def _parse_coord(coord: T.Optional[str], ref: T.Optional[str]) -> T.Optional[float]:
82
+ def _parse_coord(coord: str | None, ref: str | None) -> float | None:
83
83
  if coord is None:
84
84
  return None
85
85
  parsed = _parse_coord_numeric(coord, ref)
@@ -88,7 +88,7 @@ def _parse_coord(coord: T.Optional[str], ref: T.Optional[str]) -> T.Optional[flo
88
88
  return parsed
89
89
 
90
90
 
91
- def _parse_iso(dtstr: str) -> T.Optional[datetime.datetime]:
91
+ def _parse_iso(dtstr: str) -> datetime.datetime | None:
92
92
  try:
93
93
  return datetime.datetime.fromisoformat(dtstr)
94
94
  except ValueError:
@@ -99,8 +99,8 @@ def _parse_iso(dtstr: str) -> T.Optional[datetime.datetime]:
99
99
 
100
100
 
101
101
  def strptime_alternative_formats(
102
- dtstr: str, formats: T.Sequence[str]
103
- ) -> T.Optional[datetime.datetime]:
102
+ dtstr: str, formats: list[str]
103
+ ) -> datetime.datetime | None:
104
104
  for format in formats:
105
105
  if format == "ISO":
106
106
  dt = _parse_iso(dtstr)
@@ -114,7 +114,7 @@ def strptime_alternative_formats(
114
114
  return None
115
115
 
116
116
 
117
- def parse_timestr_as_timedelta(timestr: str) -> T.Optional[datetime.timedelta]:
117
+ def parse_timestr_as_timedelta(timestr: str) -> datetime.timedelta | None:
118
118
  timestr = timestr.strip()
119
119
  parts = timestr.strip().split(":")
120
120
  try:
@@ -133,8 +133,8 @@ def parse_timestr_as_timedelta(timestr: str) -> T.Optional[datetime.timedelta]:
133
133
 
134
134
 
135
135
  def parse_time_ratios_as_timedelta(
136
- time_tuple: T.Sequence[Ratio],
137
- ) -> T.Optional[datetime.timedelta]:
136
+ time_tuple: list[Ratio],
137
+ ) -> datetime.timedelta | None:
138
138
  try:
139
139
  hours, minutes, seconds, *_ = time_tuple
140
140
  except (ValueError, TypeError):
@@ -156,8 +156,8 @@ def parse_time_ratios_as_timedelta(
156
156
 
157
157
  def parse_gps_datetime(
158
158
  dtstr: str,
159
- default_tz: T.Optional[datetime.timezone] = datetime.timezone.utc,
160
- ) -> T.Optional[datetime.datetime]:
159
+ default_tz: datetime.timezone | None = datetime.timezone.utc,
160
+ ) -> datetime.datetime | None:
161
161
  dtstr = dtstr.strip()
162
162
 
163
163
  dt = strptime_alternative_formats(dtstr, ["ISO"])
@@ -176,8 +176,8 @@ def parse_gps_datetime(
176
176
  def parse_gps_datetime_separately(
177
177
  datestr: str,
178
178
  timestr: str,
179
- default_tz: T.Optional[datetime.timezone] = datetime.timezone.utc,
180
- ) -> T.Optional[datetime.datetime]:
179
+ default_tz: datetime.timezone | None = datetime.timezone.utc,
180
+ ) -> datetime.datetime | None:
181
181
  """
182
182
  Parse GPSDateStamp and GPSTimeStamp and return the corresponding datetime object in GMT.
183
183
 
@@ -232,8 +232,8 @@ def parse_gps_datetime_separately(
232
232
 
233
233
 
234
234
  def parse_datetimestr_with_subsec_and_offset(
235
- dtstr: str, subsec: T.Optional[str] = None, tz_offset: T.Optional[str] = None
236
- ) -> T.Optional[datetime.datetime]:
235
+ dtstr: str, subsec: str | None = None, tz_offset: str | None = None
236
+ ) -> datetime.datetime | None:
237
237
  """
238
238
  Convert dtstr "YYYY:mm:dd HH:MM:SS[.sss]" to a datetime object.
239
239
  It handles time "24:00:00" as "00:00:00" of the next day.
@@ -294,35 +294,35 @@ _FIELD_TYPE = T.TypeVar("_FIELD_TYPE", int, float, str)
294
294
 
295
295
  class ExifReadABC(abc.ABC):
296
296
  @abc.abstractmethod
297
- def extract_altitude(self) -> T.Optional[float]:
297
+ def extract_altitude(self) -> float | None:
298
298
  raise NotImplementedError
299
299
 
300
300
  @abc.abstractmethod
301
- def extract_capture_time(self) -> T.Optional[datetime.datetime]:
301
+ def extract_capture_time(self) -> datetime.datetime | None:
302
302
  raise NotImplementedError
303
303
 
304
304
  @abc.abstractmethod
305
- def extract_direction(self) -> T.Optional[float]:
305
+ def extract_direction(self) -> float | None:
306
306
  raise NotImplementedError
307
307
 
308
308
  @abc.abstractmethod
309
- def extract_lon_lat(self) -> T.Optional[T.Tuple[float, float]]:
309
+ def extract_lon_lat(self) -> tuple[float, float] | None:
310
310
  raise NotImplementedError
311
311
 
312
312
  @abc.abstractmethod
313
- def extract_make(self) -> T.Optional[str]:
313
+ def extract_make(self) -> str | None:
314
314
  raise NotImplementedError
315
315
 
316
316
  @abc.abstractmethod
317
- def extract_model(self) -> T.Optional[str]:
317
+ def extract_model(self) -> str | None:
318
318
  raise NotImplementedError
319
319
 
320
320
  @abc.abstractmethod
321
- def extract_width(self) -> T.Optional[int]:
321
+ def extract_width(self) -> int | None:
322
322
  raise NotImplementedError
323
323
 
324
324
  @abc.abstractmethod
325
- def extract_height(self) -> T.Optional[int]:
325
+ def extract_height(self) -> int | None:
326
326
  raise NotImplementedError
327
327
 
328
328
  @abc.abstractmethod
@@ -333,7 +333,7 @@ class ExifReadABC(abc.ABC):
333
333
  class ExifReadFromXMP(ExifReadABC):
334
334
  def __init__(self, etree: et.ElementTree):
335
335
  self.etree = etree
336
- self._tags_or_attrs: T.Dict[str, str] = {}
336
+ self._tags_or_attrs: dict[str, str] = {}
337
337
  for description in self.etree.iterfind(
338
338
  ".//rdf:Description", namespaces=XMP_NAMESPACES
339
339
  ):
@@ -343,12 +343,12 @@ class ExifReadFromXMP(ExifReadABC):
343
343
  if child.text is not None:
344
344
  self._tags_or_attrs[child.tag] = child.text
345
345
 
346
- def extract_altitude(self) -> T.Optional[float]:
346
+ def extract_altitude(self) -> float | None:
347
347
  return self._extract_alternative_fields(["exif:GPSAltitude"], float)
348
348
 
349
349
  def _extract_exif_datetime(
350
350
  self, dt_tag: str, subsec_tag: str, offset_tag: str
351
- ) -> T.Optional[datetime.datetime]:
351
+ ) -> datetime.datetime | None:
352
352
  dtstr = self._extract_alternative_fields([dt_tag], str)
353
353
  if dtstr is None:
354
354
  return None
@@ -363,7 +363,7 @@ class ExifReadFromXMP(ExifReadABC):
363
363
  return None
364
364
  return dt
365
365
 
366
- def extract_exif_datetime(self) -> T.Optional[datetime.datetime]:
366
+ def extract_exif_datetime(self) -> datetime.datetime | None:
367
367
  dt = self._extract_exif_datetime(
368
368
  "exif:DateTimeOriginal",
369
369
  "exif:SubsecTimeOriginal",
@@ -382,7 +382,7 @@ class ExifReadFromXMP(ExifReadABC):
382
382
 
383
383
  return None
384
384
 
385
- def extract_gps_datetime(self) -> T.Optional[datetime.datetime]:
385
+ def extract_gps_datetime(self) -> datetime.datetime | None:
386
386
  """
387
387
  Extract timestamp from GPS field.
388
388
  """
@@ -402,7 +402,7 @@ class ExifReadFromXMP(ExifReadABC):
402
402
  # handle: exif:GPSTimeStamp="17:22:05.999000"
403
403
  return parse_gps_datetime_separately(datestr, timestr)
404
404
 
405
- def extract_capture_time(self) -> T.Optional[datetime.datetime]:
405
+ def extract_capture_time(self) -> datetime.datetime | None:
406
406
  dt = self.extract_gps_datetime()
407
407
  if dt is not None and dt.date() != datetime.date(1970, 1, 1):
408
408
  return dt
@@ -413,22 +413,22 @@ class ExifReadFromXMP(ExifReadABC):
413
413
 
414
414
  return None
415
415
 
416
- def extract_direction(self) -> T.Optional[float]:
416
+ def extract_direction(self) -> float | None:
417
417
  return self._extract_alternative_fields(
418
418
  ["exif:GPSImgDirection", "exif:GPSTrack"], float
419
419
  )
420
420
 
421
- def extract_lon_lat(self) -> T.Optional[T.Tuple[float, float]]:
421
+ def extract_lon_lat(self) -> tuple[float, float] | None:
422
422
  lat_ref = self._extract_alternative_fields(["exif:GPSLatitudeRef"], str)
423
- lat_str: T.Optional[str] = self._extract_alternative_fields(
423
+ lat_str: str | None = self._extract_alternative_fields(
424
424
  ["exif:GPSLatitude"], str
425
425
  )
426
- lat: T.Optional[float] = _parse_coord(lat_str, lat_ref)
426
+ lat: float | None = _parse_coord(lat_str, lat_ref)
427
427
  if lat is None:
428
428
  return None
429
429
 
430
430
  lon_ref = self._extract_alternative_fields(["exif:GPSLongitudeRef"], str)
431
- lon_str: T.Optional[str] = self._extract_alternative_fields(
431
+ lon_str: str | None = self._extract_alternative_fields(
432
432
  ["exif:GPSLongitude"], str
433
433
  )
434
434
  lon = _parse_coord(lon_str, lon_ref)
@@ -437,13 +437,13 @@ class ExifReadFromXMP(ExifReadABC):
437
437
 
438
438
  return lon, lat
439
439
 
440
- def extract_make(self) -> T.Optional[str]:
440
+ def extract_make(self) -> str | None:
441
441
  make = self._extract_alternative_fields(["tiff:Make", "exifEX:LensMake"], str)
442
442
  if make is None:
443
443
  return None
444
444
  return make.strip()
445
445
 
446
- def extract_model(self) -> T.Optional[str]:
446
+ def extract_model(self) -> str | None:
447
447
  model = self._extract_alternative_fields(
448
448
  ["tiff:Model", "exifEX:LensModel"], str
449
449
  )
@@ -451,7 +451,7 @@ class ExifReadFromXMP(ExifReadABC):
451
451
  return None
452
452
  return model.strip()
453
453
 
454
- def extract_width(self) -> T.Optional[int]:
454
+ def extract_width(self) -> int | None:
455
455
  return self._extract_alternative_fields(
456
456
  [
457
457
  "exif:PixelXDimension",
@@ -461,7 +461,7 @@ class ExifReadFromXMP(ExifReadABC):
461
461
  int,
462
462
  )
463
463
 
464
- def extract_height(self) -> T.Optional[int]:
464
+ def extract_height(self) -> int | None:
465
465
  return self._extract_alternative_fields(
466
466
  [
467
467
  "exif:PixelYDimension",
@@ -513,7 +513,7 @@ class ExifReadFromXMP(ExifReadABC):
513
513
  return None
514
514
 
515
515
 
516
- def extract_xmp_efficiently(fp) -> T.Optional[str]:
516
+ def extract_xmp_efficiently(fp) -> str | None:
517
517
  """
518
518
  Extract XMP metadata from a JPEG file efficiently by reading only necessary chunks.
519
519
 
@@ -598,7 +598,7 @@ class ExifReadFromEXIF(ExifReadABC):
598
598
  EXIF class for reading exif from an image
599
599
  """
600
600
 
601
- def __init__(self, path_or_stream: T.Union[Path, T.BinaryIO]) -> None:
601
+ def __init__(self, path_or_stream: Path | T.BinaryIO) -> None:
602
602
  """
603
603
  Initialize EXIF object with FILE as filename or fileobj
604
604
  """
@@ -621,7 +621,7 @@ class ExifReadFromEXIF(ExifReadABC):
621
621
  LOG.warning("Error reading EXIF: %s", ex)
622
622
  self.tags = {}
623
623
 
624
- def extract_altitude(self) -> T.Optional[float]:
624
+ def extract_altitude(self) -> float | None:
625
625
  """
626
626
  Extract altitude
627
627
  """
@@ -634,7 +634,7 @@ class ExifReadFromEXIF(ExifReadABC):
634
634
  altitude_ref = {0: 1, 1: -1}
635
635
  return altitude * altitude_ref.get(ref, 1)
636
636
 
637
- def extract_gps_datetime(self) -> T.Optional[datetime.datetime]:
637
+ def extract_gps_datetime(self) -> datetime.datetime | None:
638
638
  """
639
639
  Extract timestamp from GPS field.
640
640
  """
@@ -662,7 +662,7 @@ class ExifReadFromEXIF(ExifReadABC):
662
662
 
663
663
  def _extract_exif_datetime(
664
664
  self, dt_tag: str, subsec_tag: str, offset_tag: str
665
- ) -> T.Optional[datetime.datetime]:
665
+ ) -> datetime.datetime | None:
666
666
  dtstr = self._extract_alternative_fields([dt_tag], field_type=str)
667
667
  if dtstr is None:
668
668
  return None
@@ -677,7 +677,7 @@ class ExifReadFromEXIF(ExifReadABC):
677
677
  return None
678
678
  return dt
679
679
 
680
- def extract_exif_datetime(self) -> T.Optional[datetime.datetime]:
680
+ def extract_exif_datetime(self) -> datetime.datetime | None:
681
681
  # EXIF DateTimeOriginal: 0x9003 (date/time when original image was taken)
682
682
  # EXIF SubSecTimeOriginal: 0x9291 (fractional seconds for DateTimeOriginal)
683
683
  # EXIF OffsetTimeOriginal: 0x9011 (time zone for DateTimeOriginal)
@@ -711,7 +711,7 @@ class ExifReadFromEXIF(ExifReadABC):
711
711
 
712
712
  return None
713
713
 
714
- def extract_capture_time(self) -> T.Optional[datetime.datetime]:
714
+ def extract_capture_time(self) -> datetime.datetime | None:
715
715
  """
716
716
  Extract capture time from EXIF DateTime tags
717
717
  """
@@ -730,7 +730,7 @@ class ExifReadFromEXIF(ExifReadABC):
730
730
 
731
731
  return None
732
732
 
733
- def extract_direction(self) -> T.Optional[float]:
733
+ def extract_direction(self) -> float | None:
734
734
  """
735
735
  Extract image direction (i.e. compass, heading, bearing)
736
736
  """
@@ -740,7 +740,7 @@ class ExifReadFromEXIF(ExifReadABC):
740
740
  ]
741
741
  return self._extract_alternative_fields(fields, float)
742
742
 
743
- def extract_lon_lat(self) -> T.Optional[T.Tuple[float, float]]:
743
+ def extract_lon_lat(self) -> tuple[float, float] | None:
744
744
  lat_tag = self.tags.get("GPS GPSLatitude")
745
745
  lon_tag = self.tags.get("GPS GPSLongitude")
746
746
  if lat_tag and lon_tag:
@@ -762,7 +762,7 @@ class ExifReadFromEXIF(ExifReadABC):
762
762
 
763
763
  return None
764
764
 
765
- def extract_make(self) -> T.Optional[str]:
765
+ def extract_make(self) -> str | None:
766
766
  """
767
767
  Extract camera make
768
768
  """
@@ -773,7 +773,7 @@ class ExifReadFromEXIF(ExifReadABC):
773
773
  return None
774
774
  return make.strip()
775
775
 
776
- def extract_model(self) -> T.Optional[str]:
776
+ def extract_model(self) -> str | None:
777
777
  """
778
778
  Extract camera model
779
779
  """
@@ -784,7 +784,7 @@ class ExifReadFromEXIF(ExifReadABC):
784
784
  return None
785
785
  return model.strip()
786
786
 
787
- def extract_width(self) -> T.Optional[int]:
787
+ def extract_width(self) -> int | None:
788
788
  """
789
789
  Extract image width in pixels
790
790
  """
@@ -792,7 +792,7 @@ class ExifReadFromEXIF(ExifReadABC):
792
792
  ["Image ImageWidth", "EXIF ExifImageWidth"], int
793
793
  )
794
794
 
795
- def extract_height(self) -> T.Optional[int]:
795
+ def extract_height(self) -> int | None:
796
796
  """
797
797
  Extract image height in pixels
798
798
  """
@@ -813,7 +813,7 @@ class ExifReadFromEXIF(ExifReadABC):
813
813
 
814
814
  def _extract_alternative_fields(
815
815
  self,
816
- fields: T.Sequence[str],
816
+ fields: T.Iterable[str],
817
817
  field_type: type[_FIELD_TYPE],
818
818
  ) -> _FIELD_TYPE | None:
819
819
  """
@@ -847,7 +847,7 @@ class ExifReadFromEXIF(ExifReadABC):
847
847
  raise ValueError(f"Invalid field type {field_type}")
848
848
  return None
849
849
 
850
- def extract_application_notes(self) -> T.Optional[str]:
850
+ def extract_application_notes(self) -> str | None:
851
851
  xmp = self.tags.get("Image ApplicationNotes")
852
852
  if xmp is None:
853
853
  return None
@@ -863,13 +863,13 @@ class ExifRead(ExifReadFromEXIF):
863
863
  NOTE: For performance reasons, XMP is only extracted if EXIF does not contain the required fields
864
864
  """
865
865
 
866
- def __init__(self, path_or_stream: T.Union[Path, T.BinaryIO]) -> None:
866
+ def __init__(self, path_or_stream: Path | T.BinaryIO) -> None:
867
867
  super().__init__(path_or_stream)
868
868
  self._path_or_stream = path_or_stream
869
869
  self._xml_extracted: bool = False
870
- self._cached_xml: T.Optional[ExifReadFromXMP] = None
870
+ self._cached_xml: ExifReadFromXMP | None = None
871
871
 
872
- def _xmp_with_reason(self, reason: str) -> T.Optional[ExifReadFromXMP]:
872
+ def _xmp_with_reason(self, reason: str) -> ExifReadFromXMP | None:
873
873
  if not self._xml_extracted:
874
874
  LOG.debug('Extracting XMP for "%s"', reason)
875
875
  self._cached_xml = self._extract_xmp()
@@ -877,7 +877,7 @@ class ExifRead(ExifReadFromEXIF):
877
877
 
878
878
  return self._cached_xml
879
879
 
880
- def _extract_xmp(self) -> T.Optional[ExifReadFromXMP]:
880
+ def _extract_xmp(self) -> ExifReadFromXMP | None:
881
881
  xml_str = self.extract_application_notes()
882
882
  if xml_str is None:
883
883
  if isinstance(self._path_or_stream, Path):
@@ -898,7 +898,7 @@ class ExifRead(ExifReadFromEXIF):
898
898
 
899
899
  return ExifReadFromXMP(et.ElementTree(e))
900
900
 
901
- def extract_altitude(self) -> T.Optional[float]:
901
+ def extract_altitude(self) -> float | None:
902
902
  val = super().extract_altitude()
903
903
  if val is not None:
904
904
  return val
@@ -910,7 +910,7 @@ class ExifRead(ExifReadFromEXIF):
910
910
  return val
911
911
  return None
912
912
 
913
- def extract_capture_time(self) -> T.Optional[datetime.datetime]:
913
+ def extract_capture_time(self) -> datetime.datetime | None:
914
914
  val = super().extract_capture_time()
915
915
  if val is not None:
916
916
  return val
@@ -922,7 +922,7 @@ class ExifRead(ExifReadFromEXIF):
922
922
  return val
923
923
  return None
924
924
 
925
- def extract_lon_lat(self) -> T.Optional[T.Tuple[float, float]]:
925
+ def extract_lon_lat(self) -> tuple[float, float] | None:
926
926
  val = super().extract_lon_lat()
927
927
  if val is not None:
928
928
  return val
@@ -934,7 +934,7 @@ class ExifRead(ExifReadFromEXIF):
934
934
  return val
935
935
  return None
936
936
 
937
- def extract_make(self) -> T.Optional[str]:
937
+ def extract_make(self) -> str | None:
938
938
  val = super().extract_make()
939
939
  if val is not None:
940
940
  return val
@@ -946,7 +946,7 @@ class ExifRead(ExifReadFromEXIF):
946
946
  return val
947
947
  return None
948
948
 
949
- def extract_model(self) -> T.Optional[str]:
949
+ def extract_model(self) -> str | None:
950
950
  val = super().extract_model()
951
951
  if val is not None:
952
952
  return val
@@ -958,7 +958,7 @@ class ExifRead(ExifReadFromEXIF):
958
958
  return val
959
959
  return None
960
960
 
961
- def extract_width(self) -> T.Optional[int]:
961
+ def extract_width(self) -> int | None:
962
962
  val = super().extract_width()
963
963
  if val is not None:
964
964
  return val
@@ -970,7 +970,7 @@ class ExifRead(ExifReadFromEXIF):
970
970
  return val
971
971
  return None
972
972
 
973
- def extract_height(self) -> T.Optional[int]:
973
+ def extract_height(self) -> int | None:
974
974
  val = super().extract_height()
975
975
  if val is not None:
976
976
  return val