Source code for baldaquin.timeline

# Copyright (C) 2022 the baldaquin team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""Time-related facilities.
"""

from __future__ import annotations

import calendar
from dataclasses import dataclass
import datetime
import time


[docs] class tzoffset(datetime.tzinfo): # pylint: disable=invalid-name """Minimal tzinfo class factory to create time-aware datetime objects. See https://docs.python.org/3/library/datetime.html#datetime.tzinfo for more details. Arguments --------- name : str The tzinfo object name. offset : float The UTC offset in seconds. """ def __init__(self, name: str, offset: float) -> None: """Constructor. """ self.name = name self.offset = datetime.timedelta(seconds=offset)
[docs] def utcoffset(self, dt: datetime.datetime) -> float: """Overloaded method. """ return self.offset
[docs] def dst(self, dt: datetime.datetime) -> float: """Overloaded method. According to the documentation, this Return the daylight saving time (DST) adjustment, as a timedelta object or None if DST information isn’t known. """ return None
[docs] def tzname(self, dt: datetime.datetime) -> str: """Overloaded method. """ return self.name
[docs] @dataclass class Timestamp: """Small utility class to represent a timezone-aware timestamp. A Timestamp encodes three basic pieces of information: * a datetime object in the UTC time zone; * a datetime object in the local time zone; * a timestamp in seconds, relative to the origin of the parent timeline. Timestamp objects support subtraction aritmethics as a handy shortcut to calculate time differences. Timestamp objects also support serialization/deserialization through the :meth:`to_dict() <baldaquin.timeline.Timestamp.to_dict>` and :meth:`from_dict() <baldaquin.timeline.Timestamp.from_dict>`, which, in turn, use internally a string conversion to ISO format. (``datetime`` objects are implemented in C and therefore have no ``__dict__`` slot.) Arguments --------- utc_datetime : datetime.datetime The (timezone-aware) UTC datetime object corresponding to the timestamp. local_datetime : datetime.datetime The (timezone-aware) local datetime object corresponding to the timestamp. seconds : float The seconds elapsed since the origin of the parent timeline. """ utc_datetime: datetime.datetime local_datetime: datetime.datetime seconds: float # These are the fields that need special handling when serializing/deserializing. _DATETIME_FIELDS = ('utc_datetime', 'local_datetime')
[docs] def to_dict(self) -> dict: """Serialization. """ dict_ = {**self.__dict__} for key in self._DATETIME_FIELDS: dict_.update({key: dict_[key].isoformat()}) return dict_
[docs] @classmethod def from_dict(cls, **kwargs) -> 'Timestamp': """Deserialization. """ for key in cls._DATETIME_FIELDS: kwargs.update({key: datetime.datetime.fromisoformat(kwargs[key])}) return cls(**kwargs)
def __sub__(self, other: Timestamp) -> float: """Overloaded operator to support timestamp subtraction. """ return self.seconds - other.seconds def __str__(self) -> str: """String formatting. """ return f'{self.local_datetime} ({self.seconds} s)'
[docs] class Timeline: # pylint: disable=too-few-public-methods """Class representing a continuos timeline referred to a fixed origin. Note that, by deafult, the origin of the Timeline is January 1, 1970 00:00:00 UTC, and the seconds field in the Timestamp objects that the Timeline returns when latched correspond to the standard POSIX time. Setting the origin to a different value allow, e.g., to emulate the mission elapsed time (MET) concept that is common in space missions. Arguments --------- origin : str A string representation (in ISO 8601 format) of the date and time corresponding to the origin of the timeline in UTC (without the traling +00:00). More specifically, according to the datetime documentation, we support strings in the format: .. code-block:: YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]] where * can match any single character. """ _POSIX_ORIGIN = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) def __init__(self, origin: str = '1970-01-01') -> None: """Constructor. """ self.origin = datetime.datetime.fromisoformat(f'{origin}') self.origin = self.origin.replace(tzinfo=datetime.timezone.utc) self._timestamp_offset = (self.origin - self._POSIX_ORIGIN).total_seconds()
[docs] @staticmethod def _utc_offset() -> int: """Return the local UTC offset in s, considering the DST---note this has to be calculated every time the timeline is latched, as one doesn't know if a DST change has happened between two successive calls. See https://stackoverflow.com/questions/3168096 for more details on why this is a sensible way to calculate this. """ return calendar.timegm(time.localtime()) - calendar.timegm(time.gmtime())
[docs] def latch(self) -> Timestamp: """This is the workhorse function for keeping track of the time. This function latches the system time and creates a frozen Timestamp object that can be reused at later time. """ # Retrieve the UTC date and time---this is preferred over datetime.utcnow(), # as the latter returns a naive datetime object, with tzinfo set to None. utc_datetime = datetime.datetime.now(datetime.timezone.utc) # Calculate the UTC offset. offset = self._utc_offset() # Add the offset to the UTC datetime and setup the tzinfo so that # the offset is included by default in the string representation. local_datetime = utc_datetime + datetime.timedelta(seconds=offset) local_datetime = local_datetime.replace(tzinfo=tzoffset('Local', offset)) seconds = utc_datetime.timestamp() - self._timestamp_offset return Timestamp(utc_datetime, local_datetime, seconds)