Skip to content

Commit 5fb87e2

Browse files
committed
Accept ISO 8601 (with timezone) for --video_start_time
The --video_start_time option only accepted the proprietary YYYY_MM_DD_HH_MM_SS_sss format, which is always interpreted as UTC. This left no clean way to correct timestamps for cameras whose RTC has no timezone and records local time (e.g. GoPro MAX), as reported in #819. Add a _parse_video_start_time() helper that also accepts ISO 8601: - an explicit UTC offset (or Z) is honored, and - a naive value is interpreted in the system local timezone. The legacy format is unchanged and still treated as UTC. Documented via doctests and updated the CLI help text.
1 parent a880c7d commit 5fb87e2

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

mapillary_tools/commands/sample_video.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def add_basic_arguments(self, parser: argparse.ArgumentParser):
4141
)
4242
group.add_argument(
4343
"--video_start_time",
44-
help="Video start time specified in YYYY_MM_DD_HH_MM_SS_sss in UTC. For example 2020_12_28_12_36_36_508 represents 2020-12-28T12:36:36.508Z.",
44+
help="Video start time, either as YYYY_MM_DD_HH_MM_SS_sss in UTC (for example 2020_12_28_12_36_36_508 represents 2020-12-28T12:36:36.508Z) or as ISO 8601, which may include a UTC offset (for example 2020-12-28T13:36:36.508+01:00). Use the ISO 8601 form for cameras that record local time, such as GoPro MAX. An ISO 8601 value without an offset is interpreted in the system local timezone.",
4545
default=None,
4646
required=False,
4747
)

mapillary_tools/sample_video.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,60 @@
2323
LOG = logging.getLogger(__name__)
2424

2525

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+
2680
def _normalize_path(
2781
video_import_path: Path, skip_subfolders: bool
2882
) -> tuple[Path, list[Path]]:
@@ -71,7 +125,7 @@ def sample_video(
71125
video_start_time_dt: datetime.datetime | None = None
72126
if video_start_time is not None:
73127
try:
74-
video_start_time_dt = parse_capture_time(video_start_time)
128+
video_start_time_dt = _parse_video_start_time(video_start_time)
75129
except ValueError as ex:
76130
raise exceptions.MapillaryBadParameterError(str(ex))
77131

0 commit comments

Comments
 (0)