Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mapillary_tools/commands/sample_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def add_basic_arguments(self, parser: argparse.ArgumentParser):
)
group.add_argument(
"--video_start_time",
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.",
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.",
default=None,
required=False,
)
Expand Down
56 changes: 55 additions & 1 deletion mapillary_tools/sample_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,60 @@
LOG = logging.getLogger(__name__)


def _parse_video_start_time(value: str) -> datetime.datetime:
"""
Parse a ``--video_start_time`` value into a timezone-aware UTC datetime.

Two formats are accepted:

- The legacy proprietary format ``YYYY_MM_DD_HH_MM_SS_sss``, which is
always interpreted as UTC.
- ISO 8601, which may carry a UTC offset (e.g.
``2020-12-28T12:36:36.508+01:00`` or ``...Z``). This lets users of
cameras that write local time (e.g. GoPro MAX, whose RTC has no
timezone) correct the offset. A naive ISO 8601 value (no offset) is
interpreted in the system's local timezone, so a wall-clock time
copied from such a camera lands at the right instant.

Legacy format is always UTC:

>>> _parse_video_start_time("2020_12_28_12_36_36_508").isoformat()
'2020-12-28T12:36:36.508000+00:00'

ISO 8601 with an explicit offset (or ``Z``) keeps that offset:

>>> _parse_video_start_time("2020-12-28T12:36:36.508+01:00").isoformat()
'2020-12-28T11:36:36.508000+00:00'
>>> _parse_video_start_time("2020-12-28T12:36:36.508Z").isoformat()
'2020-12-28T12:36:36.508000+00:00'

Naive ISO 8601 is interpreted in the system local timezone (the result
below is shown relative to local time so the doctest is tz-independent):

>>> naive = "2020-12-28T13:36:36.508"
>>> expected = datetime.datetime(
... 2020, 12, 28, 13, 36, 36, 508000
... ).astimezone(datetime.timezone.utc)
>>> _parse_video_start_time(naive) == expected
True

>>> _parse_video_start_time("not-a-timestamp")
Traceback (most recent call last):
...
ValueError: Invalid isoformat string: 'not-a-timestamp'
"""
try:
return parse_capture_time(value)
except ValueError:
pass

# datetime.fromisoformat does not accept a trailing "Z" before Python 3.11
dt = datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
# A naive value is assumed to be in the system local timezone; astimezone()
# treats naive datetimes as local and converts aware ones by their offset.
return dt.astimezone(datetime.timezone.utc)


def _normalize_path(
video_import_path: Path, skip_subfolders: bool
) -> tuple[Path, list[Path]]:
Expand Down Expand Up @@ -71,7 +125,7 @@ def sample_video(
video_start_time_dt: datetime.datetime | None = None
if video_start_time is not None:
try:
video_start_time_dt = parse_capture_time(video_start_time)
video_start_time_dt = _parse_video_start_time(video_start_time)
except ValueError as ex:
raise exceptions.MapillaryBadParameterError(str(ex))

Expand Down
Loading