Source code for task_script_utils.datetime_parser.ts_datetime

import copy
from datetime import datetime
from typing import Optional

import pendulum

from .parser_exceptions import AmbiguousFoldError


[docs] class TSDatetime: """TSDatetime wraps the parsed datetime and subsecond values and provide formatting functions like tsformat and isoformat """ # pylint: disable=E0601 def __init__(self, datetime_: datetime, subseconds: Optional[str] = None): if not isinstance(datetime_, datetime): raise TypeError("datetime_ must be a datetime object") self._datetime: datetime = datetime_ self._subseconds: Optional[str] = subseconds @property def subseconds(self) -> Optional[str]: """ Subseconds represent fractional part of seconds value that was parsed from raw datetime as string. eg: In `05-26-1991 12:12:23.000000023`, `000000023` as string would represent subseconds. Subseconds is None when it is not present in raw datetime string """ return self._subseconds @property def tzinfo(self) -> datetime.tzinfo: """Returns tzinfo for the stored datetime object""" return self._datetime.tzinfo @property def datetime(self) -> datetime: """Return a new `datetime` object after replacing `microseconds` with `self._subseconds` in `self._datetime`. `subseconds` needs to be truncated to 6 digits """ if self._subseconds is None: return self._datetime microseconds = self._subseconds[:6] microseconds = datetime.strptime(microseconds, "%f").microsecond new_datetime = self._datetime.replace(microsecond=microseconds) return new_datetime
[docs] def tsformat(self) -> str: """Returns datetime string in Tetrascience's ISO8601 DateTime format""" minimal_format = "YYYY-MM-DDTHH:mm:ss" if self.tzinfo is not None: utc = pendulum.tz.UTC utc_date = utc.convert(self._datetime) iso_8601 = utc_date.format(minimal_format) if self._subseconds is not None: iso_8601 = f"{iso_8601}.{self._subseconds}" iso_8601 = f"{iso_8601}Z" else: iso_8601 = self._datetime.format(minimal_format) if self._subseconds is not None: iso_8601 = f"{iso_8601}.{self._subseconds}" iso_8601 = self._pad_year(iso_8601) return iso_8601
[docs] def isoformat(self) -> str: """Returns datetime string in ISO format with offset values""" iso_str = self._datetime.format("YYYY-MM-DDTHH:mm:ss") if self._subseconds: iso_str += f".{str(self._subseconds)}" if self.tzinfo: offset = self._datetime.format("ZZ") if ":" not in offset: offset = f"{offset[:3]}:{offset[-2:]}" iso_str += offset iso_str = self._pad_year(iso_str) return iso_str
[docs] def change_fold(self, new_fold: Optional[int]): """Replace the fold value of stored datetime object if needed. """ if new_fold is None: if self._is_fold_required: raise AmbiguousFoldError( "DatetimeConfig.fold must not be None to parse datetime " f"{self.tsformat()} without ambiguity." ) # Setting fold=None has no effect. Return early. return if self._datetime.tzinfo is None or new_fold == self._datetime.fold: return self._datetime = self._datetime.replace(fold=new_fold)
@property def _is_fold_required(self) -> bool: """Check whether an undefined `fold` would cause an ambiguous TSDatetime This function returns True if "fold" is required to disambiguate a datetime, False otherwise. """ # Copy because TSDatetime is mutable date_time = copy.deepcopy(self) date_time.change_fold(0) dt_before_fold = date_time.tsformat() date_time.change_fold(1) dt_after_fold = date_time.tsformat() return dt_before_fold != dt_after_fold @staticmethod def _pad_year(datetime_str: str) -> str: """Make sure that year is padded to 4 digits. for example, Input = 1-01-01T12:12:!2 Output = 0001-01-01T12:12:12 """ dt_tokens = datetime_str.split("-") dt_tokens[0] = f"{int(dt_tokens[0]):04d}" return "-".join(dt_tokens)