|
23 | 23 | LOG = logging.getLogger(__name__) |
24 | 24 |
|
25 | 25 |
|
| 26 | +def _parse_video_start_time(value: str) -> datetime.datetime: |
| 27 | + """ |
| 28 | + Parse a ``--video_start_time`` value into a timezone-aware UTC datetime. |
| 29 | +
|
| 30 | + Two formats are accepted: |
| 31 | +
|
| 32 | + - The legacy proprietary format ``YYYY_MM_DD_HH_MM_SS_sss``, which is |
| 33 | + always interpreted as UTC. |
| 34 | + - ISO 8601, which may carry a UTC offset (e.g. |
| 35 | + ``2020-12-28T12:36:36.508+01:00`` or ``...Z``). This lets users of |
| 36 | + cameras that write local time (e.g. GoPro MAX, whose RTC has no |
| 37 | + timezone) correct the offset. A naive ISO 8601 value (no offset) is |
| 38 | + interpreted in the system's local timezone, so a wall-clock time |
| 39 | + copied from such a camera lands at the right instant. |
| 40 | +
|
| 41 | + Legacy format is always UTC: |
| 42 | +
|
| 43 | + >>> _parse_video_start_time("2020_12_28_12_36_36_508").isoformat() |
| 44 | + '2020-12-28T12:36:36.508000+00:00' |
| 45 | +
|
| 46 | + ISO 8601 with an explicit offset (or ``Z``) keeps that offset: |
| 47 | +
|
| 48 | + >>> _parse_video_start_time("2020-12-28T12:36:36.508+01:00").isoformat() |
| 49 | + '2020-12-28T11:36:36.508000+00:00' |
| 50 | + >>> _parse_video_start_time("2020-12-28T12:36:36.508Z").isoformat() |
| 51 | + '2020-12-28T12:36:36.508000+00:00' |
| 52 | +
|
| 53 | + Naive ISO 8601 is interpreted in the system local timezone (the result |
| 54 | + below is shown relative to local time so the doctest is tz-independent): |
| 55 | +
|
| 56 | + >>> naive = "2020-12-28T13:36:36.508" |
| 57 | + >>> expected = datetime.datetime( |
| 58 | + ... 2020, 12, 28, 13, 36, 36, 508000 |
| 59 | + ... ).astimezone(datetime.timezone.utc) |
| 60 | + >>> _parse_video_start_time(naive) == expected |
| 61 | + True |
| 62 | +
|
| 63 | + >>> _parse_video_start_time("not-a-timestamp") |
| 64 | + Traceback (most recent call last): |
| 65 | + ... |
| 66 | + ValueError: Invalid isoformat string: 'not-a-timestamp' |
| 67 | + """ |
| 68 | + try: |
| 69 | + return parse_capture_time(value) |
| 70 | + except ValueError: |
| 71 | + pass |
| 72 | + |
| 73 | + # datetime.fromisoformat does not accept a trailing "Z" before Python 3.11 |
| 74 | + dt = datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) |
| 75 | + # A naive value is assumed to be in the system local timezone; astimezone() |
| 76 | + # treats naive datetimes as local and converts aware ones by their offset. |
| 77 | + return dt.astimezone(datetime.timezone.utc) |
| 78 | + |
| 79 | + |
26 | 80 | def _normalize_path( |
27 | 81 | video_import_path: Path, skip_subfolders: bool |
28 | 82 | ) -> tuple[Path, list[Path]]: |
@@ -71,7 +125,7 @@ def sample_video( |
71 | 125 | video_start_time_dt: datetime.datetime | None = None |
72 | 126 | if video_start_time is not None: |
73 | 127 | try: |
74 | | - video_start_time_dt = parse_capture_time(video_start_time) |
| 128 | + video_start_time_dt = _parse_video_start_time(video_start_time) |
75 | 129 | except ValueError as ex: |
76 | 130 | raise exceptions.MapillaryBadParameterError(str(ex)) |
77 | 131 |
|
|
0 commit comments