|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import re |
| 4 | +import warnings |
| 5 | +from datetime import datetime, timezone |
| 6 | +from typing import Literal |
4 | 7 |
|
5 | | -import pendulum |
6 | | -from pendulum.datetime import DateTime |
| 8 | +from whenever import Date, Instant, LocalDateTime, Time, ZonedDateTime |
| 9 | + |
| 10 | +from .exceptions import TimestampFormatError |
| 11 | + |
| 12 | +UTC = timezone.utc # Required for older versions of Python |
7 | 13 |
|
8 | 14 | REGEX_MAPPING = { |
9 | 15 | "seconds": r"(\d+)(s|sec|second|seconds)", |
|
12 | 18 | } |
13 | 19 |
|
14 | 20 |
|
15 | | -class TimestampFormatError(ValueError): ... |
16 | | - |
17 | | - |
18 | 21 | class Timestamp: |
19 | | - def __init__(self, value: str | DateTime | Timestamp | None = None): |
20 | | - if value and isinstance(value, DateTime): |
21 | | - self.obj = value |
| 22 | + _obj: ZonedDateTime |
| 23 | + |
| 24 | + def __init__(self, value: str | ZonedDateTime | Timestamp | None = None): |
| 25 | + if value and isinstance(value, ZonedDateTime): |
| 26 | + self._obj = value |
22 | 27 | elif value and isinstance(value, self.__class__): |
23 | | - self.obj = value.obj |
| 28 | + self._obj = value._obj |
24 | 29 | elif isinstance(value, str): |
25 | | - self.obj = self._parse_string(value) |
| 30 | + self._obj = self._parse_string(value) |
26 | 31 | else: |
27 | | - self.obj = DateTime.now(tz="UTC") |
| 32 | + self._obj = ZonedDateTime.now("UTC").round(unit="microsecond") |
| 33 | + |
| 34 | + @property |
| 35 | + def obj(self) -> ZonedDateTime: |
| 36 | + warnings.warn( |
| 37 | + "Direct access to obj property is deprecated. Use to_string(), to_timestamp(), or to_datetime() instead.", |
| 38 | + UserWarning, |
| 39 | + stacklevel=2, |
| 40 | + ) |
| 41 | + return self._obj |
28 | 42 |
|
29 | 43 | @classmethod |
30 | | - def _parse_string(cls, value: str) -> DateTime: |
| 44 | + def _parse_string(cls, value: str) -> ZonedDateTime: |
| 45 | + try: |
| 46 | + zoned_date = ZonedDateTime.parse_common_iso(value) |
| 47 | + return zoned_date |
| 48 | + except ValueError: |
| 49 | + pass |
| 50 | + |
31 | 51 | try: |
32 | | - parsed_date = pendulum.parse(value) |
33 | | - if isinstance(parsed_date, DateTime): |
34 | | - return parsed_date |
35 | | - except (pendulum.parsing.exceptions.ParserError, ValueError): |
| 52 | + instant_date = Instant.parse_common_iso(value) |
| 53 | + return instant_date.to_tz("UTC") |
| 54 | + except ValueError: |
36 | 55 | pass |
37 | 56 |
|
38 | | - params = {} |
| 57 | + try: |
| 58 | + local_date_time = LocalDateTime.parse_common_iso(value) |
| 59 | + return local_date_time.assume_utc().to_tz("UTC") |
| 60 | + except ValueError: |
| 61 | + pass |
| 62 | + |
| 63 | + try: |
| 64 | + date = Date.parse_common_iso(value) |
| 65 | + local_date = date.at(Time(12, 00)) |
| 66 | + return local_date.assume_tz("UTC", disambiguate="compatible") |
| 67 | + except ValueError: |
| 68 | + pass |
| 69 | + |
| 70 | + params: dict[str, float] = {} |
39 | 71 | for key, regex in REGEX_MAPPING.items(): |
40 | 72 | match = re.search(regex, value) |
41 | 73 | if match: |
42 | | - params[key] = int(match.group(1)) |
| 74 | + params[key] = float(match.group(1)) |
43 | 75 |
|
44 | | - if not params: |
45 | | - raise TimestampFormatError(f"Invalid time format for {value}") |
| 76 | + if params: |
| 77 | + return ZonedDateTime.now("UTC").subtract(**params) # type: ignore[call-overload] |
46 | 78 |
|
47 | | - return DateTime.now(tz="UTC").subtract(**params) |
| 79 | + raise TimestampFormatError(f"Invalid time format for {value}") |
48 | 80 |
|
49 | 81 | def __repr__(self) -> str: |
50 | 82 | return f"Timestamp: {self.to_string()}" |
51 | 83 |
|
52 | 84 | def to_string(self, with_z: bool = True) -> str: |
53 | | - iso8601_string = self.obj.to_iso8601_string() |
54 | | - if not with_z and iso8601_string[-1] == "Z": |
55 | | - iso8601_string = iso8601_string[:-1] + "+00:00" |
56 | | - return iso8601_string |
| 85 | + if with_z: |
| 86 | + return self._obj.instant().format_common_iso() |
| 87 | + return self.to_datetime().isoformat() |
57 | 88 |
|
58 | 89 | def to_timestamp(self) -> int: |
59 | | - return self.obj.int_timestamp |
| 90 | + return self._obj.timestamp() |
| 91 | + |
| 92 | + def to_datetime(self) -> datetime: |
| 93 | + return self._obj.py_datetime() |
| 94 | + |
| 95 | + def get_obj(self) -> ZonedDateTime: |
| 96 | + return self._obj |
60 | 97 |
|
61 | 98 | def __eq__(self, other: object) -> bool: |
62 | 99 | if not isinstance(other, Timestamp): |
63 | 100 | return NotImplemented |
64 | | - return self.obj == other.obj |
| 101 | + return self._obj == other._obj |
65 | 102 |
|
66 | 103 | def __lt__(self, other: object) -> bool: |
67 | 104 | if not isinstance(other, Timestamp): |
68 | 105 | return NotImplemented |
69 | | - return self.obj < other.obj |
| 106 | + return self._obj < other._obj |
70 | 107 |
|
71 | 108 | def __gt__(self, other: object) -> bool: |
72 | 109 | if not isinstance(other, Timestamp): |
73 | 110 | return NotImplemented |
74 | | - return self.obj > other.obj |
| 111 | + return self._obj > other._obj |
75 | 112 |
|
76 | 113 | def __le__(self, other: object) -> bool: |
77 | 114 | if not isinstance(other, Timestamp): |
78 | 115 | return NotImplemented |
79 | | - return self.obj <= other.obj |
| 116 | + return self._obj <= other._obj |
80 | 117 |
|
81 | 118 | def __ge__(self, other: object) -> bool: |
82 | 119 | if not isinstance(other, Timestamp): |
83 | 120 | return NotImplemented |
84 | | - return self.obj >= other.obj |
| 121 | + return self._obj >= other._obj |
85 | 122 |
|
86 | 123 | def __hash__(self) -> int: |
87 | 124 | return hash(self.to_string()) |
88 | 125 |
|
89 | 126 | def add_delta(self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0) -> Timestamp: |
90 | | - time = self.obj.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds) |
91 | | - return Timestamp(time) |
| 127 | + warnings.warn( |
| 128 | + "add_delta() is deprecated. Use add() instead.", |
| 129 | + UserWarning, |
| 130 | + stacklevel=2, |
| 131 | + ) |
| 132 | + return self.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds) |
| 133 | + |
| 134 | + def add( |
| 135 | + self, |
| 136 | + years: int = 0, |
| 137 | + months: int = 0, |
| 138 | + weeks: int = 0, |
| 139 | + days: int = 0, |
| 140 | + hours: float = 0, |
| 141 | + minutes: float = 0, |
| 142 | + seconds: float = 0, |
| 143 | + milliseconds: float = 0, |
| 144 | + microseconds: float = 0, |
| 145 | + nanoseconds: int = 0, |
| 146 | + disambiguate: Literal["compatible"] = "compatible", |
| 147 | + ) -> Timestamp: |
| 148 | + return Timestamp( |
| 149 | + self._obj.add( |
| 150 | + years=years, |
| 151 | + months=months, |
| 152 | + weeks=weeks, |
| 153 | + days=days, |
| 154 | + hours=hours, |
| 155 | + minutes=minutes, |
| 156 | + seconds=seconds, |
| 157 | + milliseconds=milliseconds, |
| 158 | + microseconds=microseconds, |
| 159 | + nanoseconds=nanoseconds, |
| 160 | + disambiguate=disambiguate, |
| 161 | + ) |
| 162 | + ) |
| 163 | + |
| 164 | + def subtract( |
| 165 | + self, |
| 166 | + years: int = 0, |
| 167 | + months: int = 0, |
| 168 | + weeks: int = 0, |
| 169 | + days: int = 0, |
| 170 | + hours: float = 0, |
| 171 | + minutes: float = 0, |
| 172 | + seconds: float = 0, |
| 173 | + milliseconds: float = 0, |
| 174 | + microseconds: float = 0, |
| 175 | + nanoseconds: int = 0, |
| 176 | + disambiguate: Literal["compatible"] = "compatible", |
| 177 | + ) -> Timestamp: |
| 178 | + return Timestamp( |
| 179 | + self._obj.subtract( |
| 180 | + years=years, |
| 181 | + months=months, |
| 182 | + weeks=weeks, |
| 183 | + days=days, |
| 184 | + hours=hours, |
| 185 | + minutes=minutes, |
| 186 | + seconds=seconds, |
| 187 | + milliseconds=milliseconds, |
| 188 | + microseconds=microseconds, |
| 189 | + nanoseconds=nanoseconds, |
| 190 | + disambiguate=disambiguate, |
| 191 | + ) |
| 192 | + ) |
0 commit comments