dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dycw_utilities-0.175.17.dist-info/METADATA +34 -0
- dycw_utilities-0.175.17.dist-info/RECORD +103 -0
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +14 -14
- utilities/asyncio.py +350 -819
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +77 -22
- utilities/cachetools.py +24 -29
- utilities/click.py +393 -237
- utilities/concurrent.py +8 -11
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +83 -118
- utilities/docker.py +293 -0
- utilities/enum.py +26 -23
- utilities/errors.py +17 -3
- utilities/fastapi.py +29 -65
- utilities/fpdf2.py +3 -3
- utilities/functions.py +169 -416
- utilities/functools.py +18 -19
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +738 -589
- utilities/importlib.py +17 -1
- utilities/inflect.py +25 -0
- utilities/iterables.py +194 -262
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +5 -9
- utilities/logging.py +345 -543
- utilities/math.py +18 -13
- utilities/memory_profiler.py +11 -15
- utilities/more_itertools.py +200 -131
- utilities/operator.py +33 -29
- utilities/optuna.py +6 -6
- utilities/orjson.py +272 -137
- utilities/os.py +61 -4
- utilities/parse.py +59 -61
- utilities/pathlib.py +281 -40
- utilities/permissions.py +298 -0
- utilities/pickle.py +2 -2
- utilities/platform.py +24 -5
- utilities/polars.py +1214 -430
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +113 -26
- utilities/pqdm.py +10 -11
- utilities/psutil.py +6 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -54
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +8 -10
- utilities/pytest.py +227 -121
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +13 -9
- utilities/re.py +58 -28
- utilities/redis.py +401 -550
- utilities/scipy.py +1 -1
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +36 -106
- utilities/sqlalchemy.py +502 -473
- utilities/sqlalchemy_polars.py +38 -94
- utilities/string.py +2 -3
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +86 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +37 -65
- utilities/traceback.py +158 -929
- utilities/types.py +146 -116
- utilities/typing.py +531 -71
- utilities/tzdata.py +1 -53
- utilities/tzlocal.py +6 -23
- utilities/uuid.py +43 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1776 -386
- utilities/zoneinfo.py +84 -22
- dycw_utilities-0.129.10.dist-info/METADATA +0 -241
- dycw_utilities-0.129.10.dist-info/RECORD +0 -96
- dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
- dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
- utilities/datetime.py +0 -1409
- utilities/eventkit.py +0 -402
- utilities/loguru.py +0 -144
- utilities/luigi.py +0 -228
- utilities/period.py +0 -324
- utilities/pyrsistent.py +0 -89
- utilities/python_dotenv.py +0 -105
- utilities/streamlit.py +0 -105
- utilities/sys.py +0 -87
- utilities/tenacity.py +0 -145
utilities/whenever.py
CHANGED
|
@@ -1,565 +1,1775 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime as dt
|
|
4
|
-
import
|
|
5
|
-
from contextlib import suppress
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from functools import cache
|
|
8
7
|
from logging import LogRecord
|
|
9
|
-
from
|
|
8
|
+
from statistics import fmean
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
Any,
|
|
12
|
+
Literal,
|
|
13
|
+
Self,
|
|
14
|
+
SupportsFloat,
|
|
15
|
+
TypedDict,
|
|
16
|
+
assert_never,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
override,
|
|
20
|
+
)
|
|
21
|
+
from zoneinfo import ZoneInfo
|
|
10
22
|
|
|
11
23
|
from whenever import (
|
|
12
24
|
Date,
|
|
25
|
+
DateDelta,
|
|
13
26
|
DateTimeDelta,
|
|
14
27
|
PlainDateTime,
|
|
15
28
|
Time,
|
|
16
|
-
|
|
29
|
+
TimeDelta,
|
|
30
|
+
Weekday,
|
|
31
|
+
YearMonth,
|
|
17
32
|
ZonedDateTime,
|
|
18
33
|
)
|
|
19
34
|
|
|
20
|
-
from utilities.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
from utilities.math import ParseNumberError, parse_number
|
|
29
|
-
from utilities.re import (
|
|
30
|
-
ExtractGroupError,
|
|
31
|
-
ExtractGroupsError,
|
|
32
|
-
extract_group,
|
|
33
|
-
extract_groups,
|
|
34
|
-
)
|
|
35
|
-
from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
|
|
35
|
+
from utilities.dataclasses import replace_non_sentinel
|
|
36
|
+
from utilities.functions import get_class_name
|
|
37
|
+
from utilities.math import sign
|
|
38
|
+
from utilities.platform import get_strftime
|
|
39
|
+
from utilities.sentinel import Sentinel, sentinel
|
|
40
|
+
from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
|
|
41
|
+
from utilities.zoneinfo import UTC, to_time_zone_name
|
|
36
42
|
|
|
37
43
|
if TYPE_CHECKING:
|
|
38
|
-
from zoneinfo import ZoneInfo
|
|
39
|
-
|
|
40
44
|
from utilities.types import (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
DateOrDateTimeDelta,
|
|
46
|
+
DateTimeRoundMode,
|
|
47
|
+
Delta,
|
|
48
|
+
MaybeCallableDateLike,
|
|
49
|
+
MaybeCallableTimeLike,
|
|
50
|
+
MaybeCallableZonedDateTimeLike,
|
|
51
|
+
TimeOrDateTimeDelta,
|
|
52
|
+
TimeZoneLike,
|
|
47
53
|
)
|
|
48
54
|
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
# bounds
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key)
|
|
60
|
+
ZONED_DATE_TIME_MAX = PlainDateTime.MAX.assume_tz(UTC.key)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
DATE_TIME_DELTA_MIN = DateTimeDelta(
|
|
64
|
+
weeks=-521722,
|
|
65
|
+
days=-5,
|
|
66
|
+
hours=-23,
|
|
67
|
+
minutes=-59,
|
|
68
|
+
seconds=-59,
|
|
69
|
+
milliseconds=-999,
|
|
70
|
+
microseconds=-999,
|
|
71
|
+
nanoseconds=-999,
|
|
72
|
+
)
|
|
73
|
+
DATE_TIME_DELTA_MAX = DateTimeDelta(
|
|
74
|
+
weeks=521722,
|
|
75
|
+
days=5,
|
|
76
|
+
hours=23,
|
|
77
|
+
minutes=59,
|
|
78
|
+
seconds=59,
|
|
79
|
+
milliseconds=999,
|
|
80
|
+
microseconds=999,
|
|
81
|
+
nanoseconds=999,
|
|
82
|
+
)
|
|
83
|
+
DATE_DELTA_MIN = DATE_TIME_DELTA_MIN.date_part()
|
|
84
|
+
DATE_DELTA_MAX = DATE_TIME_DELTA_MAX.date_part()
|
|
85
|
+
TIME_DELTA_MIN = TimeDelta(hours=-87831216)
|
|
86
|
+
TIME_DELTA_MAX = TimeDelta(hours=87831216)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta(
|
|
90
|
+
weeks=-142857,
|
|
91
|
+
hours=-23,
|
|
92
|
+
minutes=-59,
|
|
93
|
+
seconds=-59,
|
|
94
|
+
milliseconds=-999,
|
|
95
|
+
microseconds=-999,
|
|
96
|
+
nanoseconds=-999,
|
|
97
|
+
)
|
|
98
|
+
DATE_TIME_DELTA_PARSABLE_MAX = DateTimeDelta(
|
|
99
|
+
weeks=142857,
|
|
100
|
+
hours=23,
|
|
101
|
+
minutes=59,
|
|
102
|
+
seconds=59,
|
|
103
|
+
milliseconds=999,
|
|
104
|
+
microseconds=999,
|
|
105
|
+
nanoseconds=999,
|
|
106
|
+
)
|
|
107
|
+
DATE_DELTA_PARSABLE_MIN = DateDelta(days=-999999)
|
|
108
|
+
DATE_DELTA_PARSABLE_MAX = DateDelta(days=999999)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
DATE_TWO_DIGIT_YEAR_MIN = Date(1969, 1, 1)
|
|
112
|
+
DATE_TWO_DIGIT_YEAR_MAX = Date(DATE_TWO_DIGIT_YEAR_MIN.year + 99, 12, 31)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## common constants
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
ZERO_DAYS = DateDelta()
|
|
119
|
+
ZERO_TIME = TimeDelta()
|
|
120
|
+
MICROSECOND = TimeDelta(microseconds=1)
|
|
121
|
+
MILLISECOND = TimeDelta(milliseconds=1)
|
|
122
|
+
SECOND = TimeDelta(seconds=1)
|
|
123
|
+
MINUTE = TimeDelta(minutes=1)
|
|
124
|
+
HOUR = TimeDelta(hours=1)
|
|
125
|
+
DAY = DateDelta(days=1)
|
|
126
|
+
WEEK = DateDelta(weeks=1)
|
|
127
|
+
MONTH = DateDelta(months=1)
|
|
128
|
+
YEAR = DateDelta(years=1)
|
|
52
129
|
|
|
53
130
|
|
|
54
131
|
##
|
|
55
132
|
|
|
56
133
|
|
|
57
|
-
def
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try: # skipif-ci-and-windows
|
|
62
|
-
result = (
|
|
63
|
-
ZonedDateTime.from_py_datetime(datetime2)
|
|
64
|
-
.to_tz(get_time_zone_name(UTC))
|
|
65
|
-
.to_tz(get_time_zone_name(time_zone))
|
|
66
|
-
.py_datetime()
|
|
67
|
-
)
|
|
68
|
-
except TimeZoneNotFoundError: # pragma: no cover
|
|
69
|
-
raise _CheckValidZonedDateTimeInvalidTimeZoneError(datetime=datetime) from None
|
|
70
|
-
if result != datetime2: # skipif-ci-and-windows
|
|
71
|
-
raise _CheckValidZonedDateTimeUnequalError(datetime=datetime, result=result)
|
|
134
|
+
def add_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearMonth:
|
|
135
|
+
"""Add to a year-month."""
|
|
136
|
+
y = x.on_day(1) + DateDelta(years=years, months=months)
|
|
137
|
+
return y.year_month()
|
|
72
138
|
|
|
73
139
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
140
|
+
def sub_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearMonth:
|
|
141
|
+
"""Subtract from a year-month."""
|
|
142
|
+
y = x.on_day(1) - DateDelta(years=years, months=months)
|
|
143
|
+
return y.year_month()
|
|
77
144
|
|
|
78
145
|
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
##
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
150
|
+
class DatePeriod:
|
|
151
|
+
"""A period of dates."""
|
|
152
|
+
|
|
153
|
+
start: Date
|
|
154
|
+
end: Date
|
|
155
|
+
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
if self.start > self.end:
|
|
158
|
+
raise DatePeriodError(start=self.start, end=self.end)
|
|
159
|
+
|
|
160
|
+
def __add__(self, other: DateDelta, /) -> Self:
|
|
161
|
+
"""Offset the period."""
|
|
162
|
+
return self.replace(start=self.start + other, end=self.end + other)
|
|
163
|
+
|
|
164
|
+
def __contains__(self, other: Date, /) -> bool:
|
|
165
|
+
"""Check if a date/datetime lies in the period."""
|
|
166
|
+
return self.start <= other <= self.end
|
|
167
|
+
|
|
81
168
|
@override
|
|
82
|
-
def
|
|
83
|
-
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
cls = get_class_name(self)
|
|
171
|
+
return f"{cls}({self.start}, {self.end})"
|
|
172
|
+
|
|
173
|
+
def __sub__(self, other: DateDelta, /) -> Self:
|
|
174
|
+
"""Offset the period."""
|
|
175
|
+
return self.replace(start=self.start - other, end=self.end - other)
|
|
176
|
+
|
|
177
|
+
def at(
|
|
178
|
+
self, obj: Time | tuple[Time, Time], /, *, time_zone: TimeZoneLike = UTC
|
|
179
|
+
) -> ZonedDateTimePeriod:
|
|
180
|
+
"""Combine a date with a time to create a datetime."""
|
|
181
|
+
match obj:
|
|
182
|
+
case Time() as time:
|
|
183
|
+
start = end = time
|
|
184
|
+
case Time() as start, Time() as end:
|
|
185
|
+
...
|
|
186
|
+
case never:
|
|
187
|
+
assert_never(never)
|
|
188
|
+
tz = to_time_zone_name(time_zone)
|
|
189
|
+
return ZonedDateTimePeriod(
|
|
190
|
+
self.start.at(start).assume_tz(tz), self.end.at(end).assume_tz(tz)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def delta(self) -> DateDelta:
|
|
195
|
+
"""The delta of the period."""
|
|
196
|
+
return self.end - self.start
|
|
197
|
+
|
|
198
|
+
def format_compact(self) -> str:
|
|
199
|
+
"""Format the period in a compact fashion."""
|
|
200
|
+
fc, start, end = format_compact, self.start, self.end
|
|
201
|
+
if self.start == self.end:
|
|
202
|
+
return f"{fc(start)}="
|
|
203
|
+
if self.start.year_month() == self.end.year_month():
|
|
204
|
+
return f"{fc(start)}-{fc(end, fmt='%d')}"
|
|
205
|
+
if self.start.year == self.end.year:
|
|
206
|
+
return f"{fc(start)}-{fc(end, fmt='%m%d')}"
|
|
207
|
+
return f"{fc(start)}-{fc(end)}"
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def from_dict(cls, mapping: PeriodDict[Date] | PeriodDict[dt.date], /) -> Self:
|
|
211
|
+
"""Convert the dictionary to a period."""
|
|
212
|
+
match mapping["start"]:
|
|
213
|
+
case Date() as start:
|
|
214
|
+
...
|
|
215
|
+
case dt.date() as py_date:
|
|
216
|
+
start = Date.from_py_date(py_date)
|
|
217
|
+
case never:
|
|
218
|
+
assert_never(never)
|
|
219
|
+
match mapping["end"]:
|
|
220
|
+
case Date() as end:
|
|
221
|
+
...
|
|
222
|
+
case dt.date() as py_date:
|
|
223
|
+
end = Date.from_py_date(py_date)
|
|
224
|
+
case never:
|
|
225
|
+
assert_never(never)
|
|
226
|
+
return cls(start=start, end=end)
|
|
227
|
+
|
|
228
|
+
def replace(
|
|
229
|
+
self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
|
|
230
|
+
) -> Self:
|
|
231
|
+
"""Replace elements of the period."""
|
|
232
|
+
return replace_non_sentinel(self, start=start, end=end)
|
|
233
|
+
|
|
234
|
+
def to_dict(self) -> PeriodDict[Date]:
|
|
235
|
+
"""Convert the period to a dictionary."""
|
|
236
|
+
return PeriodDict(start=self.start, end=self.end)
|
|
237
|
+
|
|
238
|
+
def to_py_dict(self) -> PeriodDict[dt.date]:
|
|
239
|
+
"""Convert the period to a dictionary."""
|
|
240
|
+
return PeriodDict(start=self.start.py_date(), end=self.end.py_date())
|
|
84
241
|
|
|
85
242
|
|
|
86
243
|
@dataclass(kw_only=True, slots=True)
|
|
87
|
-
class
|
|
88
|
-
|
|
244
|
+
class DatePeriodError(Exception):
|
|
245
|
+
start: Date
|
|
246
|
+
end: Date
|
|
89
247
|
|
|
90
248
|
@override
|
|
91
249
|
def __str__(self) -> str:
|
|
92
|
-
return f"
|
|
250
|
+
return f"Invalid period; got {self.start} > {self.end}"
|
|
93
251
|
|
|
94
252
|
|
|
95
253
|
##
|
|
96
254
|
|
|
97
255
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
256
|
+
def datetime_utc(
|
|
257
|
+
year: int,
|
|
258
|
+
month: int,
|
|
259
|
+
day: int,
|
|
260
|
+
/,
|
|
261
|
+
hour: int = 0,
|
|
262
|
+
minute: int = 0,
|
|
263
|
+
second: int = 0,
|
|
264
|
+
millisecond: int = 0,
|
|
265
|
+
microsecond: int = 0,
|
|
266
|
+
nanosecond: int = 0,
|
|
267
|
+
) -> ZonedDateTime:
|
|
268
|
+
"""Create a UTC-zoned datetime."""
|
|
269
|
+
nanos = int(1e6) * millisecond + int(1e3) * microsecond + nanosecond
|
|
270
|
+
return ZonedDateTime(
|
|
271
|
+
year,
|
|
272
|
+
month,
|
|
273
|
+
day,
|
|
274
|
+
hour=hour,
|
|
275
|
+
minute=minute,
|
|
276
|
+
second=second,
|
|
277
|
+
nanosecond=nanos,
|
|
278
|
+
tz=UTC.key,
|
|
279
|
+
)
|
|
107
280
|
|
|
108
281
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
282
|
+
##
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@overload
|
|
286
|
+
def diff_year_month(
|
|
287
|
+
x: YearMonth, y: YearMonth, /, *, years: Literal[True]
|
|
288
|
+
) -> tuple[int, int]: ...
|
|
289
|
+
@overload
|
|
290
|
+
def diff_year_month(
|
|
291
|
+
x: YearMonth, y: YearMonth, /, *, years: Literal[False] = False
|
|
292
|
+
) -> int: ...
|
|
293
|
+
@overload
|
|
294
|
+
def diff_year_month(
|
|
295
|
+
x: YearMonth, y: YearMonth, /, *, years: bool = False
|
|
296
|
+
) -> int | tuple[int, int]: ...
|
|
297
|
+
def diff_year_month(
|
|
298
|
+
x: YearMonth, y: YearMonth, /, *, years: bool = False
|
|
299
|
+
) -> int | tuple[int, int]:
|
|
300
|
+
"""Compute the difference between two year-months."""
|
|
301
|
+
x_date, y_date = x.on_day(1), y.on_day(1)
|
|
302
|
+
diff = x_date - y_date
|
|
303
|
+
if years:
|
|
304
|
+
yrs, mth, _ = diff.in_years_months_days()
|
|
305
|
+
return yrs, mth
|
|
306
|
+
mth, _ = diff.in_months_days()
|
|
307
|
+
return mth
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
##
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def format_compact(
|
|
314
|
+
obj: Date | Time | PlainDateTime | ZonedDateTime,
|
|
315
|
+
/,
|
|
316
|
+
*,
|
|
317
|
+
fmt: str | None = None,
|
|
318
|
+
path: bool = False,
|
|
319
|
+
) -> str:
|
|
320
|
+
"""Format the date/datetime in a compact fashion."""
|
|
321
|
+
match obj:
|
|
322
|
+
case Date() as date:
|
|
323
|
+
obj_use = date.py_date()
|
|
324
|
+
fmt_use = "%Y%m%d" if fmt is None else fmt
|
|
325
|
+
case Time() as time:
|
|
326
|
+
obj_use = time.round().py_time()
|
|
327
|
+
fmt_use = "%H%M%S" if fmt is None else fmt
|
|
328
|
+
case PlainDateTime() as date_time:
|
|
329
|
+
obj_use = date_time.round().py_datetime()
|
|
330
|
+
fmt_use = "%Y%m%dT%H%M%S" if fmt is None else fmt
|
|
331
|
+
case ZonedDateTime() as date_time:
|
|
332
|
+
plain = format_compact(date_time.to_plain(), fmt=fmt)
|
|
333
|
+
tz = date_time.tz
|
|
334
|
+
if path:
|
|
335
|
+
tz = tz.replace("/", "~")
|
|
336
|
+
return f"{plain}[{tz}]"
|
|
337
|
+
case never:
|
|
338
|
+
assert_never(never)
|
|
339
|
+
return obj_use.strftime(get_strftime(fmt_use))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
##
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def from_timestamp(i: float, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
|
|
346
|
+
"""Get a zoned datetime from a timestamp."""
|
|
347
|
+
return ZonedDateTime.from_timestamp(i, tz=to_time_zone_name(time_zone))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def from_timestamp_millis(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
|
|
351
|
+
"""Get a zoned datetime from a timestamp (in milliseconds)."""
|
|
352
|
+
return ZonedDateTime.from_timestamp_millis(i, tz=to_time_zone_name(time_zone))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def from_timestamp_nanos(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
|
|
356
|
+
"""Get a zoned datetime from a timestamp (in nanoseconds)."""
|
|
357
|
+
return ZonedDateTime.from_timestamp_nanos(i, tz=to_time_zone_name(time_zone))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
##
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def get_now(time_zone: TimeZoneLike = UTC, /) -> ZonedDateTime:
|
|
364
|
+
"""Get the current zoned date-time."""
|
|
365
|
+
return ZonedDateTime.now(to_time_zone_name(time_zone))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
NOW_UTC = get_now(UTC)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def get_now_local() -> ZonedDateTime:
|
|
372
|
+
"""Get the current zoned date-time in the local time-zone."""
|
|
373
|
+
return get_now(LOCAL_TIME_ZONE)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
NOW_LOCAL = get_now_local()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def get_now_plain(time_zone: TimeZoneLike = UTC, /) -> PlainDateTime:
|
|
380
|
+
"""Get the current plain date-time."""
|
|
381
|
+
return get_now(time_zone).to_plain()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
NOW_PLAIN = get_now_plain()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def get_now_local_plain() -> PlainDateTime:
|
|
388
|
+
"""Get the current plain date-time in the local time-zone."""
|
|
389
|
+
return get_now_local().to_plain()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
NOW_LOCAL_PLAIN = get_now_local_plain()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
##
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_time(time_zone: TimeZoneLike = UTC, /) -> Time:
|
|
399
|
+
"""Get the current time."""
|
|
400
|
+
return get_now(time_zone).time()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
TIME_UTC = get_time(UTC)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def get_time_local() -> Time:
|
|
407
|
+
"""Get the current time in the local time-zone."""
|
|
408
|
+
return get_time(LOCAL_TIME_ZONE)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
TIME_LOCAL = get_time_local()
|
|
412
|
+
|
|
112
413
|
|
|
414
|
+
##
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_today(time_zone: TimeZoneLike = UTC, /) -> Date:
|
|
418
|
+
"""Get the current, timezone-aware local date."""
|
|
419
|
+
return get_now(time_zone).date()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
TODAY_UTC = get_today(UTC)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def get_today_local() -> Date:
|
|
426
|
+
"""Get the current, timezone-aware local date."""
|
|
427
|
+
return get_today(LOCAL_TIME_ZONE)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
TODAY_LOCAL = get_today_local()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
##
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def is_weekend(
|
|
437
|
+
date_time: ZonedDateTime,
|
|
438
|
+
/,
|
|
439
|
+
*,
|
|
440
|
+
start: tuple[Weekday, Time] = (Weekday.SATURDAY, Time.MIN),
|
|
441
|
+
end: tuple[Weekday, Time] = (Weekday.SUNDAY, Time.MAX),
|
|
442
|
+
) -> bool:
|
|
443
|
+
"""Check if a datetime is in the weekend."""
|
|
444
|
+
weekday, time = date_time.date().day_of_week(), date_time.time()
|
|
445
|
+
start_weekday, start_time = start
|
|
446
|
+
end_weekday, end_time = end
|
|
447
|
+
if start_weekday.value == end_weekday.value:
|
|
448
|
+
return start_time <= time <= end_time
|
|
449
|
+
if start_weekday.value < end_weekday.value:
|
|
450
|
+
return (
|
|
451
|
+
((weekday == start_weekday) and (time >= start_time))
|
|
452
|
+
or (start_weekday.value < weekday.value < end_weekday.value)
|
|
453
|
+
or ((weekday == end_weekday) and (time <= end_time))
|
|
454
|
+
)
|
|
455
|
+
return (
|
|
456
|
+
((weekday == start_weekday) and (time >= start_time))
|
|
457
|
+
or (weekday.value > start_weekday.value)
|
|
458
|
+
or (weekday.value < end_weekday.value)
|
|
459
|
+
or ((weekday == end_weekday) and (time <= end_time))
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
##
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def mean_datetime(
|
|
467
|
+
datetimes: Iterable[ZonedDateTime],
|
|
468
|
+
/,
|
|
469
|
+
*,
|
|
470
|
+
weights: Iterable[SupportsFloat] | None = None,
|
|
471
|
+
) -> ZonedDateTime:
|
|
472
|
+
"""Compute the mean of a set of datetimes."""
|
|
473
|
+
datetimes = list(datetimes)
|
|
474
|
+
match len(datetimes):
|
|
475
|
+
case 0:
|
|
476
|
+
raise MeanDateTimeError from None
|
|
477
|
+
case 1:
|
|
478
|
+
return datetimes[0]
|
|
479
|
+
case _:
|
|
480
|
+
timestamps = [d.timestamp_nanos() for d in datetimes]
|
|
481
|
+
timestamp = round(fmean(timestamps, weights=weights))
|
|
482
|
+
return ZonedDateTime.from_timestamp_nanos(timestamp, tz=datetimes[0].tz)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@dataclass(kw_only=True, slots=True)
|
|
486
|
+
class MeanDateTimeError(Exception):
|
|
113
487
|
@override
|
|
114
488
|
def __str__(self) -> str:
|
|
115
|
-
return
|
|
489
|
+
return "Mean requires at least 1 datetime"
|
|
116
490
|
|
|
117
491
|
|
|
118
492
|
##
|
|
119
493
|
|
|
120
494
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
495
|
+
def min_max_date(
|
|
496
|
+
*,
|
|
497
|
+
min_date: Date | None = None,
|
|
498
|
+
max_date: Date | None = None,
|
|
499
|
+
min_age: DateDelta | None = None,
|
|
500
|
+
max_age: DateDelta | None = None,
|
|
501
|
+
time_zone: TimeZoneLike = UTC,
|
|
502
|
+
) -> tuple[Date | None, Date | None]:
|
|
503
|
+
"""Compute the min/max date given a combination of dates/ages."""
|
|
504
|
+
today = get_today(time_zone)
|
|
505
|
+
min_parts: list[Date] = []
|
|
506
|
+
if min_date is not None:
|
|
507
|
+
min_parts.append(min_date)
|
|
508
|
+
if max_age is not None:
|
|
509
|
+
min_parts.append(today - max_age)
|
|
510
|
+
min_date_use = max(min_parts, default=None)
|
|
511
|
+
max_parts: list[Date] = []
|
|
512
|
+
if max_date is not None:
|
|
513
|
+
max_parts.append(max_date)
|
|
514
|
+
if min_age is not None:
|
|
515
|
+
max_parts.append(today - min_age)
|
|
516
|
+
max_date_use = min(max_parts, default=None)
|
|
517
|
+
if (
|
|
518
|
+
(min_date_use is not None)
|
|
519
|
+
and (max_date_use is not None)
|
|
520
|
+
and (min_date_use > max_date_use)
|
|
521
|
+
):
|
|
522
|
+
raise _MinMaxDatePeriodError(min_date=min_date_use, max_date=max_date_use)
|
|
523
|
+
return min_date_use, max_date_use
|
|
129
524
|
|
|
130
525
|
|
|
131
526
|
@dataclass(kw_only=True, slots=True)
|
|
132
|
-
class
|
|
133
|
-
|
|
527
|
+
class MinMaxDateError(Exception):
|
|
528
|
+
min_date: Date
|
|
529
|
+
max_date: Date
|
|
134
530
|
|
|
531
|
+
|
|
532
|
+
@dataclass(kw_only=True, slots=True)
|
|
533
|
+
class _MinMaxDatePeriodError(MinMaxDateError):
|
|
135
534
|
@override
|
|
136
535
|
def __str__(self) -> str:
|
|
137
|
-
return
|
|
536
|
+
return (
|
|
537
|
+
f"Min date must be at most max date; got {self.min_date} > {self.max_date}"
|
|
538
|
+
)
|
|
138
539
|
|
|
139
540
|
|
|
140
541
|
##
|
|
141
542
|
|
|
142
543
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
544
|
+
class PeriodDict[T: Date | Time | ZonedDateTime | dt.date | dt.time | dt.datetime](
|
|
545
|
+
TypedDict
|
|
546
|
+
):
|
|
547
|
+
"""A period as a dictionary."""
|
|
548
|
+
|
|
549
|
+
start: T
|
|
550
|
+
end: T
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
##
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
type _RoundDateDailyUnit = Literal["W", "D"]
|
|
557
|
+
type _RoundDateTimeUnit = Literal["H", "M", "S", "ms", "us", "ns"]
|
|
558
|
+
type _RoundDateOrDateTimeUnit = _RoundDateDailyUnit | _RoundDateTimeUnit
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def round_date_or_date_time[T: Date | PlainDateTime | ZonedDateTime](
|
|
562
|
+
date_or_date_time: T,
|
|
563
|
+
delta: Delta,
|
|
564
|
+
/,
|
|
565
|
+
*,
|
|
566
|
+
mode: DateTimeRoundMode = "half_even",
|
|
567
|
+
weekday: Weekday | None = None,
|
|
568
|
+
) -> T:
|
|
569
|
+
"""Round a datetime."""
|
|
570
|
+
increment, unit = _round_datetime_decompose(delta)
|
|
571
|
+
match date_or_date_time, unit, weekday:
|
|
572
|
+
case Date() as date, "W" | "D", _:
|
|
573
|
+
return _round_date_weekly_or_daily(
|
|
574
|
+
date, increment, unit, mode=mode, weekday=weekday
|
|
575
|
+
)
|
|
576
|
+
case Date() as date, "H" | "M" | "S" | "ms" | "us" | "ns", _:
|
|
577
|
+
raise _RoundDateOrDateTimeDateWithIntradayDeltaError(date=date, delta=delta)
|
|
578
|
+
case (PlainDateTime() | ZonedDateTime() as date_time, "W" | "D", _):
|
|
579
|
+
return _round_date_time_weekly_or_daily(
|
|
580
|
+
date_time, increment, unit, mode=mode, weekday=weekday
|
|
581
|
+
)
|
|
582
|
+
case (
|
|
583
|
+
PlainDateTime() | ZonedDateTime() as date_time,
|
|
584
|
+
"H" | "M" | "S" | "ms" | "us" | "ns",
|
|
585
|
+
None,
|
|
586
|
+
):
|
|
587
|
+
return _round_date_time_intraday(date_time, increment, unit, mode=mode)
|
|
588
|
+
case (
|
|
589
|
+
PlainDateTime() | ZonedDateTime() as date_time,
|
|
590
|
+
"H" | "M" | "S" | "ms" | "us" | "ns",
|
|
591
|
+
Weekday(),
|
|
592
|
+
):
|
|
593
|
+
raise _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(
|
|
594
|
+
date_time=date_time, delta=delta, weekday=weekday
|
|
595
|
+
)
|
|
596
|
+
case never:
|
|
597
|
+
assert_never(never)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTimeUnit]:
|
|
601
|
+
try:
|
|
602
|
+
weeks = to_weeks(delta)
|
|
603
|
+
except ToWeeksError:
|
|
604
|
+
pass
|
|
605
|
+
else:
|
|
606
|
+
return weeks, "W"
|
|
607
|
+
try:
|
|
608
|
+
days = to_days(delta)
|
|
609
|
+
except ToDaysError:
|
|
610
|
+
pass
|
|
611
|
+
else:
|
|
612
|
+
return days, "D"
|
|
147
613
|
try:
|
|
148
|
-
|
|
149
|
-
except
|
|
150
|
-
|
|
614
|
+
hours = to_hours(delta)
|
|
615
|
+
except ToHoursError:
|
|
616
|
+
pass
|
|
617
|
+
else:
|
|
618
|
+
if (0 < hours < 24) and (24 % hours == 0):
|
|
619
|
+
return hours, "H"
|
|
620
|
+
raise _RoundDateOrDateTimeIncrementError(
|
|
621
|
+
duration=delta, increment=hours, divisor=24
|
|
622
|
+
)
|
|
623
|
+
try:
|
|
624
|
+
minutes = to_minutes(delta)
|
|
625
|
+
except ToMinutesError:
|
|
626
|
+
pass
|
|
627
|
+
else:
|
|
628
|
+
if (0 < minutes < 60) and (60 % minutes == 0):
|
|
629
|
+
return minutes, "M"
|
|
630
|
+
raise _RoundDateOrDateTimeIncrementError(
|
|
631
|
+
duration=delta, increment=minutes, divisor=60
|
|
632
|
+
)
|
|
633
|
+
try:
|
|
634
|
+
seconds = to_seconds(delta)
|
|
635
|
+
except ToSecondsError:
|
|
636
|
+
pass
|
|
637
|
+
else:
|
|
638
|
+
if (0 < seconds < 60) and (60 % seconds == 0):
|
|
639
|
+
return seconds, "S"
|
|
640
|
+
raise _RoundDateOrDateTimeIncrementError(
|
|
641
|
+
duration=delta, increment=seconds, divisor=60
|
|
642
|
+
)
|
|
643
|
+
try:
|
|
644
|
+
milliseconds = to_milliseconds(delta)
|
|
645
|
+
except ToMillisecondsError:
|
|
646
|
+
pass
|
|
647
|
+
else:
|
|
648
|
+
if (0 < milliseconds < 1000) and (1000 % milliseconds == 0):
|
|
649
|
+
return milliseconds, "ms"
|
|
650
|
+
raise _RoundDateOrDateTimeIncrementError(
|
|
651
|
+
duration=delta, increment=milliseconds, divisor=1000
|
|
652
|
+
)
|
|
653
|
+
try:
|
|
654
|
+
microseconds = to_microseconds(delta)
|
|
655
|
+
except ToMicrosecondsError:
|
|
656
|
+
pass
|
|
657
|
+
else:
|
|
658
|
+
if (0 < microseconds < 1000) and (1000 % microseconds == 0):
|
|
659
|
+
return microseconds, "us"
|
|
660
|
+
raise _RoundDateOrDateTimeIncrementError(
|
|
661
|
+
duration=delta, increment=microseconds, divisor=1000
|
|
662
|
+
)
|
|
663
|
+
try:
|
|
664
|
+
nanoseconds = to_nanoseconds(delta)
|
|
665
|
+
except ToNanosecondsError:
|
|
666
|
+
raise _RoundDateOrDateTimeInvalidDurationError(duration=delta) from None
|
|
667
|
+
if (0 < nanoseconds < 1000) and (1000 % nanoseconds == 0):
|
|
668
|
+
return nanoseconds, "ns"
|
|
669
|
+
raise _RoundDateOrDateTimeIncrementError(
|
|
670
|
+
duration=delta, increment=nanoseconds, divisor=1000
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _round_date_weekly_or_daily(
|
|
675
|
+
date: Date,
|
|
676
|
+
increment: int,
|
|
677
|
+
unit: _RoundDateDailyUnit,
|
|
678
|
+
/,
|
|
679
|
+
*,
|
|
680
|
+
mode: DateTimeRoundMode = "half_even",
|
|
681
|
+
weekday: Weekday | None = None,
|
|
682
|
+
) -> Date:
|
|
683
|
+
match unit, weekday:
|
|
684
|
+
case "W", _:
|
|
685
|
+
return _round_date_weekly(date, increment, mode=mode, weekday=weekday)
|
|
686
|
+
case "D", None:
|
|
687
|
+
return _round_date_daily(date, increment, mode=mode)
|
|
688
|
+
case "D", Weekday():
|
|
689
|
+
raise _RoundDateOrDateTimeDateWithWeekdayError(weekday=weekday)
|
|
690
|
+
case never:
|
|
691
|
+
assert_never(never)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _round_date_weekly(
|
|
695
|
+
date: Date,
|
|
696
|
+
increment: int,
|
|
697
|
+
/,
|
|
698
|
+
*,
|
|
699
|
+
mode: DateTimeRoundMode = "half_even",
|
|
700
|
+
weekday: Weekday | None = None,
|
|
701
|
+
) -> Date:
|
|
702
|
+
mapping = {
|
|
703
|
+
None: 0,
|
|
704
|
+
Weekday.MONDAY: 0,
|
|
705
|
+
Weekday.TUESDAY: 1,
|
|
706
|
+
Weekday.WEDNESDAY: 2,
|
|
707
|
+
Weekday.THURSDAY: 3,
|
|
708
|
+
Weekday.FRIDAY: 4,
|
|
709
|
+
Weekday.SATURDAY: 5,
|
|
710
|
+
Weekday.SUNDAY: 6,
|
|
711
|
+
}
|
|
712
|
+
base = Date.MIN.add(days=mapping[weekday])
|
|
713
|
+
return _round_date_daily(date, 7 * increment, mode=mode, base=base)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _round_date_daily(
|
|
717
|
+
date: Date,
|
|
718
|
+
increment: int,
|
|
719
|
+
/,
|
|
720
|
+
*,
|
|
721
|
+
mode: DateTimeRoundMode = "half_even",
|
|
722
|
+
base: Date = Date.MIN,
|
|
723
|
+
) -> Date:
|
|
724
|
+
quotient, remainder = divmod(date.days_since(base), increment)
|
|
725
|
+
match mode:
|
|
726
|
+
case "half_even":
|
|
727
|
+
threshold = increment // 2 + (quotient % 2 == 0) or 1
|
|
728
|
+
case "ceil":
|
|
729
|
+
threshold = 1
|
|
730
|
+
case "floor":
|
|
731
|
+
threshold = increment + 1
|
|
732
|
+
case "half_floor":
|
|
733
|
+
threshold = increment // 2 + 1
|
|
734
|
+
case "half_ceil":
|
|
735
|
+
threshold = increment // 2 or 1
|
|
736
|
+
case never:
|
|
737
|
+
assert_never(never)
|
|
738
|
+
round_up = remainder >= threshold
|
|
739
|
+
return base.add(days=(quotient + round_up) * increment)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _round_date_time_intraday[T: PlainDateTime | ZonedDateTime](
|
|
743
|
+
date_time: T,
|
|
744
|
+
increment: int,
|
|
745
|
+
unit: _RoundDateTimeUnit,
|
|
746
|
+
/,
|
|
747
|
+
*,
|
|
748
|
+
mode: DateTimeRoundMode = "half_even",
|
|
749
|
+
) -> T:
|
|
750
|
+
match unit:
|
|
751
|
+
case "H":
|
|
752
|
+
unit_use = "hour"
|
|
753
|
+
case "M":
|
|
754
|
+
unit_use = "minute"
|
|
755
|
+
case "S":
|
|
756
|
+
unit_use = "second"
|
|
757
|
+
case "ms":
|
|
758
|
+
unit_use = "millisecond"
|
|
759
|
+
case "us":
|
|
760
|
+
unit_use = "microsecond"
|
|
761
|
+
case "ns":
|
|
762
|
+
unit_use = "nanosecond"
|
|
763
|
+
case never:
|
|
764
|
+
assert_never(never)
|
|
765
|
+
return date_time.round(unit_use, increment=increment, mode=mode)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _round_date_time_weekly_or_daily[T: PlainDateTime | ZonedDateTime](
|
|
769
|
+
date_time: T,
|
|
770
|
+
increment: int,
|
|
771
|
+
unit: _RoundDateDailyUnit,
|
|
772
|
+
/,
|
|
773
|
+
*,
|
|
774
|
+
mode: DateTimeRoundMode = "half_even",
|
|
775
|
+
weekday: Weekday | None = None,
|
|
776
|
+
) -> T:
|
|
777
|
+
rounded = cast("T", date_time.round("day", mode=mode))
|
|
778
|
+
new_date = _round_date_weekly_or_daily(
|
|
779
|
+
rounded.date(), increment, unit, mode=mode, weekday=weekday
|
|
780
|
+
)
|
|
781
|
+
return date_time.replace_date(new_date).replace_time(Time())
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@dataclass(kw_only=True, slots=True)
|
|
785
|
+
class RoundDateOrDateTimeError(Exception): ...
|
|
151
786
|
|
|
152
787
|
|
|
153
788
|
@dataclass(kw_only=True, slots=True)
|
|
154
|
-
class
|
|
155
|
-
duration:
|
|
789
|
+
class _RoundDateOrDateTimeIncrementError(RoundDateOrDateTimeError):
|
|
790
|
+
duration: Delta
|
|
791
|
+
increment: int
|
|
792
|
+
divisor: int
|
|
156
793
|
|
|
157
794
|
@override
|
|
158
795
|
def __str__(self) -> str:
|
|
159
|
-
return f"
|
|
796
|
+
return f"Duration {self.duration} increment must be a proper divisor of {self.divisor}; got {self.increment}"
|
|
160
797
|
|
|
161
798
|
|
|
162
|
-
|
|
799
|
+
@dataclass(kw_only=True, slots=True)
|
|
800
|
+
class _RoundDateOrDateTimeInvalidDurationError(RoundDateOrDateTimeError):
|
|
801
|
+
duration: Delta
|
|
163
802
|
|
|
803
|
+
@override
|
|
804
|
+
def __str__(self) -> str:
|
|
805
|
+
return f"Duration must be valid; got {self.duration}"
|
|
164
806
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
807
|
+
|
|
808
|
+
@dataclass(kw_only=True, slots=True)
|
|
809
|
+
class _RoundDateOrDateTimeDateWithIntradayDeltaError(RoundDateOrDateTimeError):
|
|
810
|
+
date: Date
|
|
811
|
+
delta: Delta
|
|
812
|
+
|
|
813
|
+
@override
|
|
814
|
+
def __str__(self) -> str:
|
|
815
|
+
return f"Dates must not be given intraday durations; got {self.date} and {self.delta}"
|
|
173
816
|
|
|
174
817
|
|
|
175
818
|
@dataclass(kw_only=True, slots=True)
|
|
176
|
-
class
|
|
177
|
-
|
|
819
|
+
class _RoundDateOrDateTimeDateWithWeekdayError(RoundDateOrDateTimeError):
|
|
820
|
+
weekday: Weekday
|
|
178
821
|
|
|
179
822
|
@override
|
|
180
823
|
def __str__(self) -> str:
|
|
181
|
-
return f"
|
|
824
|
+
return f"Daily rounding must not be given a weekday; got {self.weekday}"
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
@dataclass(kw_only=True, slots=True)
|
|
828
|
+
class _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(RoundDateOrDateTimeError):
|
|
829
|
+
date_time: PlainDateTime | ZonedDateTime
|
|
830
|
+
delta: Delta
|
|
831
|
+
weekday: Weekday
|
|
832
|
+
|
|
833
|
+
@override
|
|
834
|
+
def __str__(self) -> str:
|
|
835
|
+
return f"Date-times and intraday rounding must not be given a weekday; got {self.date_time}, {self.delta} and {self.weekday}"
|
|
182
836
|
|
|
183
837
|
|
|
184
838
|
##
|
|
185
839
|
|
|
186
840
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
841
|
+
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
842
|
+
class TimePeriod:
|
|
843
|
+
"""A period of times."""
|
|
844
|
+
|
|
845
|
+
start: Time
|
|
846
|
+
end: Time
|
|
847
|
+
|
|
848
|
+
@override
|
|
849
|
+
def __repr__(self) -> str:
|
|
850
|
+
cls = get_class_name(self)
|
|
851
|
+
return f"{cls}({self.start}, {self.end})"
|
|
852
|
+
|
|
853
|
+
def at(
|
|
854
|
+
self, obj: Date | tuple[Date, Date], /, *, time_zone: TimeZoneLike = UTC
|
|
855
|
+
) -> ZonedDateTimePeriod:
|
|
856
|
+
"""Combine a date with a time to create a datetime."""
|
|
857
|
+
match obj:
|
|
858
|
+
case Date() as date:
|
|
859
|
+
start = end = date
|
|
860
|
+
case Date() as start, Date() as end:
|
|
861
|
+
...
|
|
862
|
+
case never:
|
|
863
|
+
assert_never(never)
|
|
864
|
+
return DatePeriod(start, end).at((self.start, self.end), time_zone=time_zone)
|
|
865
|
+
|
|
866
|
+
@classmethod
|
|
867
|
+
def from_dict(cls, mapping: PeriodDict[Time] | PeriodDict[dt.time], /) -> Self:
|
|
868
|
+
"""Convert the dictionary to a period."""
|
|
869
|
+
match mapping["start"]:
|
|
870
|
+
case Time() as start:
|
|
871
|
+
...
|
|
872
|
+
case dt.time() as py_time:
|
|
873
|
+
start = Time.from_py_time(py_time)
|
|
874
|
+
case never:
|
|
875
|
+
assert_never(never)
|
|
876
|
+
match mapping["end"]:
|
|
877
|
+
case Time() as end:
|
|
878
|
+
...
|
|
879
|
+
case dt.time() as py_time:
|
|
880
|
+
end = Time.from_py_time(py_time)
|
|
881
|
+
case never:
|
|
882
|
+
assert_never(never)
|
|
883
|
+
return cls(start=start, end=end)
|
|
884
|
+
|
|
885
|
+
def replace(
|
|
886
|
+
self, *, start: Time | Sentinel = sentinel, end: Time | Sentinel = sentinel
|
|
887
|
+
) -> Self:
|
|
888
|
+
"""Replace elements of the period."""
|
|
889
|
+
return replace_non_sentinel(self, start=start, end=end)
|
|
890
|
+
|
|
891
|
+
def to_dict(self) -> PeriodDict[Time]:
|
|
892
|
+
"""Convert the period to a dictionary."""
|
|
893
|
+
return PeriodDict(start=self.start, end=self.end)
|
|
894
|
+
|
|
895
|
+
def to_py_dict(self) -> PeriodDict[dt.time]:
|
|
896
|
+
"""Convert the period to a dictionary."""
|
|
897
|
+
return PeriodDict(start=self.start.py_time(), end=self.end.py_time())
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
##
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@overload
|
|
904
|
+
def to_date(date: Sentinel, /, *, time_zone: TimeZoneLike = UTC) -> Sentinel: ...
|
|
905
|
+
@overload
|
|
906
|
+
def to_date(
|
|
907
|
+
date: MaybeCallableDateLike | None | dt.date = get_today,
|
|
908
|
+
/,
|
|
909
|
+
*,
|
|
910
|
+
time_zone: TimeZoneLike = UTC,
|
|
911
|
+
) -> Date: ...
|
|
912
|
+
def to_date(
|
|
913
|
+
date: MaybeCallableDateLike | dt.date | None | Sentinel = get_today,
|
|
914
|
+
/,
|
|
915
|
+
*,
|
|
916
|
+
time_zone: TimeZoneLike = UTC,
|
|
917
|
+
) -> Date | Sentinel:
|
|
918
|
+
"""Convert to a date."""
|
|
919
|
+
match date:
|
|
920
|
+
case Date() | Sentinel():
|
|
921
|
+
return date
|
|
922
|
+
case None:
|
|
923
|
+
return get_today(time_zone)
|
|
924
|
+
case str():
|
|
925
|
+
return Date.parse_iso(date)
|
|
926
|
+
case dt.date():
|
|
927
|
+
return Date.from_py_date(date)
|
|
928
|
+
case Callable() as func:
|
|
929
|
+
return to_date(func(), time_zone=time_zone)
|
|
930
|
+
case never:
|
|
931
|
+
assert_never(never)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
##
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def to_date_time_delta(nanos: int, /) -> DateTimeDelta:
|
|
938
|
+
"""Construct a date-time delta."""
|
|
939
|
+
components = _to_time_delta_components(nanos)
|
|
940
|
+
days, hours = divmod(components.hours, 24)
|
|
941
|
+
weeks, days = divmod(days, 7)
|
|
942
|
+
match sign(nanos): # pragma: no cover
|
|
943
|
+
case 1:
|
|
944
|
+
if hours < 0:
|
|
945
|
+
hours += 24
|
|
946
|
+
days -= 1
|
|
947
|
+
if days < 0:
|
|
948
|
+
days += 7
|
|
949
|
+
weeks -= 1
|
|
950
|
+
case -1:
|
|
951
|
+
if hours > 0:
|
|
952
|
+
hours -= 24
|
|
953
|
+
days += 1
|
|
954
|
+
if days > 0:
|
|
955
|
+
days -= 7
|
|
956
|
+
weeks += 1
|
|
957
|
+
case 0:
|
|
958
|
+
...
|
|
959
|
+
return DateTimeDelta(
|
|
960
|
+
weeks=weeks,
|
|
961
|
+
days=days,
|
|
962
|
+
hours=hours,
|
|
963
|
+
minutes=components.minutes,
|
|
964
|
+
seconds=components.seconds,
|
|
965
|
+
microseconds=components.microseconds,
|
|
966
|
+
milliseconds=components.milliseconds,
|
|
967
|
+
nanoseconds=components.nanoseconds,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
##
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def to_days(delta: Delta, /) -> int:
|
|
975
|
+
"""Compute the number of days in a delta."""
|
|
976
|
+
match delta:
|
|
977
|
+
case DateDelta():
|
|
978
|
+
months, days = delta.in_months_days()
|
|
979
|
+
if months != 0:
|
|
980
|
+
raise _ToDaysMonthsError(delta=delta, months=months)
|
|
981
|
+
return days
|
|
982
|
+
case TimeDelta():
|
|
983
|
+
nanos = to_nanoseconds(delta)
|
|
984
|
+
days, remainder = divmod(nanos, 24 * 60 * 60 * int(1e9))
|
|
985
|
+
if remainder != 0:
|
|
986
|
+
raise _ToDaysNanosecondsError(delta=delta, nanoseconds=remainder)
|
|
987
|
+
return days
|
|
988
|
+
case DateTimeDelta():
|
|
989
|
+
try:
|
|
990
|
+
return to_days(delta.date_part()) + to_days(delta.time_part())
|
|
991
|
+
except _ToDaysMonthsError as error:
|
|
992
|
+
raise _ToDaysMonthsError(delta=delta, months=error.months) from None
|
|
993
|
+
except _ToDaysNanosecondsError as error:
|
|
994
|
+
raise _ToDaysNanosecondsError(
|
|
995
|
+
delta=delta, nanoseconds=error.nanoseconds
|
|
996
|
+
) from None
|
|
997
|
+
case never:
|
|
998
|
+
assert_never(never)
|
|
195
999
|
|
|
196
1000
|
|
|
197
1001
|
@dataclass(kw_only=True, slots=True)
|
|
198
|
-
class
|
|
199
|
-
|
|
1002
|
+
class ToDaysError(Exception): ...
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
@dataclass(kw_only=True, slots=True)
|
|
1006
|
+
class _ToDaysMonthsError(ToDaysError):
|
|
1007
|
+
delta: DateOrDateTimeDelta
|
|
1008
|
+
months: int
|
|
200
1009
|
|
|
201
1010
|
@override
|
|
202
1011
|
def __str__(self) -> str:
|
|
203
|
-
return f"
|
|
1012
|
+
return f"Delta must not contain months; got {self.months}"
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@dataclass(kw_only=True, slots=True)
|
|
1016
|
+
class _ToDaysNanosecondsError(ToDaysError):
|
|
1017
|
+
delta: TimeOrDateTimeDelta
|
|
1018
|
+
nanoseconds: int
|
|
1019
|
+
|
|
1020
|
+
@override
|
|
1021
|
+
def __str__(self) -> str:
|
|
1022
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
204
1023
|
|
|
205
1024
|
|
|
206
1025
|
##
|
|
207
1026
|
|
|
208
1027
|
|
|
209
|
-
def
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1028
|
+
def to_hours(delta: Delta, /) -> int:
|
|
1029
|
+
"""Compute the number of hours in a delta."""
|
|
1030
|
+
match delta:
|
|
1031
|
+
case DateDelta():
|
|
1032
|
+
try:
|
|
1033
|
+
days = to_days(delta)
|
|
1034
|
+
except _ToDaysMonthsError as error:
|
|
1035
|
+
raise _ToHoursMonthsError(delta=delta, months=error.months) from None
|
|
1036
|
+
return 24 * days
|
|
1037
|
+
case TimeDelta():
|
|
1038
|
+
nanos = to_nanoseconds(delta)
|
|
1039
|
+
divisor = 60 * 60 * int(1e9)
|
|
1040
|
+
hours, remainder = divmod(nanos, divisor)
|
|
1041
|
+
if remainder != 0:
|
|
1042
|
+
raise _ToHoursNanosecondsError(delta=delta, nanoseconds=remainder)
|
|
1043
|
+
return hours
|
|
1044
|
+
case DateTimeDelta():
|
|
1045
|
+
try:
|
|
1046
|
+
return to_hours(delta.date_part()) + to_hours(delta.time_part())
|
|
1047
|
+
except _ToHoursMonthsError as error:
|
|
1048
|
+
raise _ToHoursMonthsError(delta=delta, months=error.months) from None
|
|
1049
|
+
except _ToHoursNanosecondsError as error:
|
|
1050
|
+
raise _ToHoursNanosecondsError(
|
|
1051
|
+
delta=delta, nanoseconds=error.nanoseconds
|
|
1052
|
+
) from None
|
|
1053
|
+
case never:
|
|
1054
|
+
assert_never(never)
|
|
221
1055
|
|
|
222
1056
|
|
|
223
1057
|
@dataclass(kw_only=True, slots=True)
|
|
224
|
-
class
|
|
225
|
-
timedelta: str
|
|
1058
|
+
class ToHoursError(Exception): ...
|
|
226
1059
|
|
|
227
1060
|
|
|
228
1061
|
@dataclass(kw_only=True, slots=True)
|
|
229
|
-
class
|
|
1062
|
+
class _ToHoursMonthsError(ToHoursError):
|
|
1063
|
+
delta: DateOrDateTimeDelta
|
|
1064
|
+
months: int
|
|
1065
|
+
|
|
230
1066
|
@override
|
|
231
1067
|
def __str__(self) -> str:
|
|
232
|
-
return f"
|
|
1068
|
+
return f"Delta must not contain months; got {self.months}"
|
|
233
1069
|
|
|
234
1070
|
|
|
235
1071
|
@dataclass(kw_only=True, slots=True)
|
|
236
|
-
class
|
|
1072
|
+
class _ToHoursNanosecondsError(ToHoursError):
|
|
1073
|
+
delta: TimeOrDateTimeDelta
|
|
237
1074
|
nanoseconds: int
|
|
238
1075
|
|
|
239
1076
|
@override
|
|
240
1077
|
def __str__(self) -> str:
|
|
241
|
-
return f"
|
|
1078
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
242
1079
|
|
|
243
1080
|
|
|
244
1081
|
##
|
|
245
1082
|
|
|
246
1083
|
|
|
247
|
-
def
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
1084
|
+
def to_microseconds(delta: Delta, /) -> int:
|
|
1085
|
+
"""Compute the number of microseconds in a delta."""
|
|
1086
|
+
match delta:
|
|
1087
|
+
case DateDelta():
|
|
1088
|
+
try:
|
|
1089
|
+
days = to_days(delta)
|
|
1090
|
+
except _ToDaysMonthsError as error:
|
|
1091
|
+
raise _ToMicrosecondsMonthsError(
|
|
1092
|
+
delta=delta, months=error.months
|
|
1093
|
+
) from None
|
|
1094
|
+
return 24 * 60 * 60 * int(1e6) * days
|
|
1095
|
+
case TimeDelta():
|
|
1096
|
+
nanos = to_nanoseconds(delta)
|
|
1097
|
+
microseconds, remainder = divmod(nanos, int(1e3))
|
|
1098
|
+
if remainder != 0:
|
|
1099
|
+
raise _ToMicrosecondsNanosecondsError(
|
|
1100
|
+
delta=delta, nanoseconds=remainder
|
|
1101
|
+
)
|
|
1102
|
+
return microseconds
|
|
1103
|
+
case DateTimeDelta():
|
|
1104
|
+
try:
|
|
1105
|
+
return to_microseconds(delta.date_part()) + to_microseconds(
|
|
1106
|
+
delta.time_part()
|
|
1107
|
+
)
|
|
1108
|
+
except _ToMicrosecondsMonthsError as error:
|
|
1109
|
+
raise _ToMicrosecondsMonthsError(
|
|
1110
|
+
delta=delta, months=error.months
|
|
1111
|
+
) from None
|
|
1112
|
+
except _ToMicrosecondsNanosecondsError as error:
|
|
1113
|
+
raise _ToMicrosecondsNanosecondsError(
|
|
1114
|
+
delta=delta, nanoseconds=error.nanoseconds
|
|
1115
|
+
) from None
|
|
1116
|
+
case never:
|
|
1117
|
+
assert_never(never)
|
|
255
1118
|
|
|
256
1119
|
|
|
257
1120
|
@dataclass(kw_only=True, slots=True)
|
|
258
|
-
class
|
|
259
|
-
|
|
1121
|
+
class ToMicrosecondsError(Exception): ...
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
@dataclass(kw_only=True, slots=True)
|
|
1125
|
+
class _ToMicrosecondsMonthsError(ToMicrosecondsError):
|
|
1126
|
+
delta: DateOrDateTimeDelta
|
|
1127
|
+
months: int
|
|
1128
|
+
|
|
1129
|
+
@override
|
|
1130
|
+
def __str__(self) -> str:
|
|
1131
|
+
return f"Delta must not contain months; got {self.months}"
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
@dataclass(kw_only=True, slots=True)
|
|
1135
|
+
class _ToMicrosecondsNanosecondsError(ToMicrosecondsError):
|
|
1136
|
+
delta: TimeOrDateTimeDelta
|
|
1137
|
+
nanoseconds: int
|
|
260
1138
|
|
|
261
1139
|
@override
|
|
262
1140
|
def __str__(self) -> str:
|
|
263
|
-
return f"
|
|
1141
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
264
1142
|
|
|
265
1143
|
|
|
266
1144
|
##
|
|
267
1145
|
|
|
268
1146
|
|
|
269
|
-
|
|
1147
|
+
def to_milliseconds(delta: Delta, /) -> int:
|
|
1148
|
+
"""Compute the number of milliseconds in a delta."""
|
|
1149
|
+
match delta:
|
|
1150
|
+
case DateDelta():
|
|
1151
|
+
try:
|
|
1152
|
+
days = to_days(delta)
|
|
1153
|
+
except _ToDaysMonthsError as error:
|
|
1154
|
+
raise _ToMillisecondsMonthsError(
|
|
1155
|
+
delta=delta, months=error.months
|
|
1156
|
+
) from None
|
|
1157
|
+
return 24 * 60 * 60 * int(1e3) * days
|
|
1158
|
+
case TimeDelta():
|
|
1159
|
+
nanos = to_nanoseconds(delta)
|
|
1160
|
+
milliseconds, remainder = divmod(nanos, int(1e6))
|
|
1161
|
+
if remainder != 0:
|
|
1162
|
+
raise _ToMillisecondsNanosecondsError(
|
|
1163
|
+
delta=delta, nanoseconds=remainder
|
|
1164
|
+
)
|
|
1165
|
+
return milliseconds
|
|
1166
|
+
case DateTimeDelta():
|
|
1167
|
+
try:
|
|
1168
|
+
return to_milliseconds(delta.date_part()) + to_milliseconds(
|
|
1169
|
+
delta.time_part()
|
|
1170
|
+
)
|
|
1171
|
+
except _ToMillisecondsMonthsError as error:
|
|
1172
|
+
raise _ToMillisecondsMonthsError(
|
|
1173
|
+
delta=delta, months=error.months
|
|
1174
|
+
) from None
|
|
1175
|
+
except _ToMillisecondsNanosecondsError as error:
|
|
1176
|
+
raise _ToMillisecondsNanosecondsError(
|
|
1177
|
+
delta=delta, nanoseconds=error.nanoseconds
|
|
1178
|
+
) from None
|
|
1179
|
+
case never:
|
|
1180
|
+
assert_never(never)
|
|
270
1181
|
|
|
271
1182
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
try:
|
|
275
|
-
w_date = Date.parse_common_iso(date)
|
|
276
|
-
except ValueError:
|
|
277
|
-
try:
|
|
278
|
-
((year2, month, day),) = _PARSE_DATE_YYMMDD_REGEX.findall(date)
|
|
279
|
-
except ValueError:
|
|
280
|
-
raise ParseDateError(date=date) from None
|
|
281
|
-
year = parse_two_digit_year(year2)
|
|
282
|
-
return dt.date(year=int(year), month=int(month), day=int(day))
|
|
283
|
-
return w_date.py_date()
|
|
1183
|
+
@dataclass(kw_only=True, slots=True)
|
|
1184
|
+
class ToMillisecondsError(Exception): ...
|
|
284
1185
|
|
|
285
1186
|
|
|
286
1187
|
@dataclass(kw_only=True, slots=True)
|
|
287
|
-
class
|
|
288
|
-
|
|
1188
|
+
class _ToMillisecondsMonthsError(ToMillisecondsError):
|
|
1189
|
+
delta: DateOrDateTimeDelta
|
|
1190
|
+
months: int
|
|
289
1191
|
|
|
290
1192
|
@override
|
|
291
1193
|
def __str__(self) -> str:
|
|
292
|
-
return f"
|
|
1194
|
+
return f"Delta must not contain months; got {self.months}"
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@dataclass(kw_only=True, slots=True)
|
|
1198
|
+
class _ToMillisecondsNanosecondsError(ToMillisecondsError):
|
|
1199
|
+
delta: TimeOrDateTimeDelta
|
|
1200
|
+
nanoseconds: int
|
|
1201
|
+
|
|
1202
|
+
@override
|
|
1203
|
+
def __str__(self) -> str:
|
|
1204
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
293
1205
|
|
|
294
1206
|
|
|
295
1207
|
##
|
|
296
1208
|
|
|
297
1209
|
|
|
298
|
-
def
|
|
299
|
-
"""
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
1210
|
+
def to_minutes(delta: Delta, /) -> int:
|
|
1211
|
+
"""Compute the number of minutes in a delta."""
|
|
1212
|
+
match delta:
|
|
1213
|
+
case DateDelta():
|
|
1214
|
+
try:
|
|
1215
|
+
days = to_days(delta)
|
|
1216
|
+
except _ToDaysMonthsError as error:
|
|
1217
|
+
raise _ToMinutesMonthsError(delta=delta, months=error.months) from None
|
|
1218
|
+
return 24 * 60 * days
|
|
1219
|
+
case TimeDelta():
|
|
1220
|
+
nanos = to_nanoseconds(delta)
|
|
1221
|
+
minutes, remainder = divmod(nanos, 60 * int(1e9))
|
|
1222
|
+
if remainder != 0:
|
|
1223
|
+
raise _ToMinutesNanosecondsError(delta=delta, nanoseconds=remainder)
|
|
1224
|
+
return minutes
|
|
1225
|
+
case DateTimeDelta():
|
|
1226
|
+
try:
|
|
1227
|
+
return to_minutes(delta.date_part()) + to_minutes(delta.time_part())
|
|
1228
|
+
except _ToMinutesMonthsError as error:
|
|
1229
|
+
raise _ToMinutesMonthsError(delta=delta, months=error.months) from None
|
|
1230
|
+
except _ToMinutesNanosecondsError as error:
|
|
1231
|
+
raise _ToMinutesNanosecondsError(
|
|
1232
|
+
delta=delta, nanoseconds=error.nanoseconds
|
|
1233
|
+
) from None
|
|
1234
|
+
case never:
|
|
1235
|
+
assert_never(never)
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
@dataclass(kw_only=True, slots=True)
|
|
1239
|
+
class ToMinutesError(Exception): ...
|
|
305
1240
|
|
|
306
1241
|
|
|
307
1242
|
@dataclass(kw_only=True, slots=True)
|
|
308
|
-
class
|
|
309
|
-
|
|
1243
|
+
class _ToMinutesMonthsError(ToMinutesError):
|
|
1244
|
+
delta: DateOrDateTimeDelta
|
|
1245
|
+
months: int
|
|
310
1246
|
|
|
311
1247
|
@override
|
|
312
1248
|
def __str__(self) -> str:
|
|
313
|
-
return f"
|
|
1249
|
+
return f"Delta must not contain months; got {self.months}"
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
@dataclass(kw_only=True, slots=True)
|
|
1253
|
+
class _ToMinutesNanosecondsError(ToMinutesError):
|
|
1254
|
+
delta: TimeOrDateTimeDelta
|
|
1255
|
+
nanoseconds: int
|
|
1256
|
+
|
|
1257
|
+
@override
|
|
1258
|
+
def __str__(self) -> str:
|
|
1259
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
314
1260
|
|
|
315
1261
|
|
|
316
1262
|
##
|
|
317
1263
|
|
|
318
1264
|
|
|
319
|
-
def
|
|
320
|
-
"""
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
1265
|
+
def to_months(delta: DateOrDateTimeDelta, /) -> int:
|
|
1266
|
+
"""Compute the number of months in a delta."""
|
|
1267
|
+
match delta:
|
|
1268
|
+
case DateDelta():
|
|
1269
|
+
months, days = delta.in_months_days()
|
|
1270
|
+
if days != 0:
|
|
1271
|
+
raise _ToMonthsDaysError(delta=delta, days=days)
|
|
1272
|
+
return months
|
|
1273
|
+
case DateTimeDelta():
|
|
1274
|
+
if delta.time_part() != TimeDelta():
|
|
1275
|
+
raise _ToMonthsTimeError(delta=delta)
|
|
1276
|
+
try:
|
|
1277
|
+
return to_months(delta.date_part())
|
|
1278
|
+
except _ToMonthsDaysError as error:
|
|
1279
|
+
raise _ToMonthsDaysError(delta=delta, days=error.days) from None
|
|
1280
|
+
case never:
|
|
1281
|
+
assert_never(never)
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
@dataclass(kw_only=True, slots=True)
|
|
1285
|
+
class ToMonthsError(Exception): ...
|
|
327
1286
|
|
|
328
1287
|
|
|
329
1288
|
@dataclass(kw_only=True, slots=True)
|
|
330
|
-
class
|
|
331
|
-
|
|
1289
|
+
class _ToMonthsDaysError(ToMonthsError):
|
|
1290
|
+
delta: DateOrDateTimeDelta
|
|
1291
|
+
days: int
|
|
332
1292
|
|
|
333
1293
|
@override
|
|
334
1294
|
def __str__(self) -> str:
|
|
335
|
-
return f"
|
|
1295
|
+
return f"Delta must not contain days; got {self.days}"
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
@dataclass(kw_only=True, slots=True)
|
|
1299
|
+
class _ToMonthsTimeError(ToMonthsError):
|
|
1300
|
+
delta: DateTimeDelta
|
|
1301
|
+
|
|
1302
|
+
@override
|
|
1303
|
+
def __str__(self) -> str:
|
|
1304
|
+
return f"Delta must not contain a time part; got {self.delta.time_part()}"
|
|
336
1305
|
|
|
337
1306
|
|
|
338
1307
|
##
|
|
339
1308
|
|
|
340
1309
|
|
|
341
|
-
def
|
|
342
|
-
"""
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1310
|
+
def to_months_and_days(delta: DateOrDateTimeDelta, /) -> tuple[int, int]:
|
|
1311
|
+
"""Compute the number of months & days in a delta."""
|
|
1312
|
+
match delta:
|
|
1313
|
+
case DateDelta():
|
|
1314
|
+
return delta.in_months_days()
|
|
1315
|
+
case DateTimeDelta():
|
|
1316
|
+
if delta.time_part() != TimeDelta():
|
|
1317
|
+
raise ToMonthsAndDaysError(delta=delta)
|
|
1318
|
+
return to_months_and_days(delta.date_part())
|
|
1319
|
+
case never:
|
|
1320
|
+
assert_never(never)
|
|
348
1321
|
|
|
349
1322
|
|
|
350
1323
|
@dataclass(kw_only=True, slots=True)
|
|
351
|
-
class
|
|
352
|
-
|
|
1324
|
+
class ToMonthsAndDaysError(Exception):
|
|
1325
|
+
delta: DateTimeDelta
|
|
353
1326
|
|
|
354
1327
|
@override
|
|
355
1328
|
def __str__(self) -> str:
|
|
356
|
-
return f"
|
|
1329
|
+
return f"Delta must not contain a time part; got {self.delta.time_part()}"
|
|
357
1330
|
|
|
358
1331
|
|
|
359
1332
|
##
|
|
360
1333
|
|
|
361
1334
|
|
|
362
|
-
def
|
|
363
|
-
"""
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1335
|
+
def to_nanoseconds(delta: Delta, /) -> int:
|
|
1336
|
+
"""Compute the number of nanoseconds in a date-time delta."""
|
|
1337
|
+
match delta:
|
|
1338
|
+
case DateDelta():
|
|
1339
|
+
try:
|
|
1340
|
+
days = to_days(delta)
|
|
1341
|
+
except _ToDaysMonthsError as error:
|
|
1342
|
+
raise ToNanosecondsError(delta=delta, months=error.months) from None
|
|
1343
|
+
return 24 * 60 * 60 * int(1e9) * days
|
|
1344
|
+
case TimeDelta():
|
|
1345
|
+
return delta.in_nanoseconds()
|
|
1346
|
+
case DateTimeDelta():
|
|
1347
|
+
try:
|
|
1348
|
+
return to_nanoseconds(delta.date_part()) + to_nanoseconds(
|
|
1349
|
+
delta.time_part()
|
|
1350
|
+
)
|
|
1351
|
+
except ToNanosecondsError as error:
|
|
1352
|
+
raise ToNanosecondsError(delta=delta, months=error.months) from None
|
|
1353
|
+
case never:
|
|
1354
|
+
assert_never(never)
|
|
369
1355
|
|
|
370
1356
|
|
|
371
1357
|
@dataclass(kw_only=True, slots=True)
|
|
372
|
-
class
|
|
373
|
-
|
|
1358
|
+
class ToNanosecondsError(Exception):
|
|
1359
|
+
delta: DateOrDateTimeDelta
|
|
1360
|
+
months: int
|
|
374
1361
|
|
|
375
1362
|
@override
|
|
376
1363
|
def __str__(self) -> str:
|
|
377
|
-
return f"
|
|
1364
|
+
return f"Delta must not contain months; got {self.months}"
|
|
378
1365
|
|
|
379
1366
|
|
|
380
1367
|
##
|
|
381
1368
|
|
|
382
1369
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
)
|
|
402
|
-
time = dt.timedelta(microseconds=int(time_part.in_microseconds()))
|
|
403
|
-
return days + time
|
|
1370
|
+
@overload
|
|
1371
|
+
def to_py_date_or_date_time(date_or_date_time: Date, /) -> dt.date: ...
|
|
1372
|
+
@overload
|
|
1373
|
+
def to_py_date_or_date_time(date_or_date_time: ZonedDateTime, /) -> dt.datetime: ...
|
|
1374
|
+
@overload
|
|
1375
|
+
def to_py_date_or_date_time(date_or_date_time: None, /) -> None: ...
|
|
1376
|
+
def to_py_date_or_date_time(
|
|
1377
|
+
date_or_date_time: Date | ZonedDateTime | None, /
|
|
1378
|
+
) -> dt.date | None:
|
|
1379
|
+
"""Convert a Date or ZonedDateTime into a standard library equivalent."""
|
|
1380
|
+
match date_or_date_time:
|
|
1381
|
+
case Date() as date:
|
|
1382
|
+
return date.py_date()
|
|
1383
|
+
case ZonedDateTime() as date_time:
|
|
1384
|
+
return date_time.py_datetime()
|
|
1385
|
+
case None:
|
|
1386
|
+
return None
|
|
1387
|
+
case never:
|
|
1388
|
+
assert_never(never)
|
|
404
1389
|
|
|
405
1390
|
|
|
406
|
-
|
|
407
|
-
class ParseTimedeltaError(Exception):
|
|
408
|
-
timedelta: str
|
|
1391
|
+
##
|
|
409
1392
|
|
|
410
1393
|
|
|
411
|
-
@
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1394
|
+
@overload
|
|
1395
|
+
def to_py_time_delta(delta: Delta, /) -> dt.timedelta: ...
|
|
1396
|
+
@overload
|
|
1397
|
+
def to_py_time_delta(delta: None, /) -> None: ...
|
|
1398
|
+
def to_py_time_delta(delta: Delta | None, /) -> dt.timedelta | None:
|
|
1399
|
+
"""Try convert a DateDelta to a standard library timedelta."""
|
|
1400
|
+
match delta:
|
|
1401
|
+
case DateDelta():
|
|
1402
|
+
return dt.timedelta(days=to_days(delta))
|
|
1403
|
+
case TimeDelta():
|
|
1404
|
+
nanos = delta.in_nanoseconds()
|
|
1405
|
+
micros, remainder = divmod(nanos, 1000)
|
|
1406
|
+
if remainder != 0:
|
|
1407
|
+
raise ToPyTimeDeltaError(nanoseconds=remainder)
|
|
1408
|
+
return dt.timedelta(microseconds=micros)
|
|
1409
|
+
case DateTimeDelta():
|
|
1410
|
+
return to_py_time_delta(delta.date_part()) + to_py_time_delta(
|
|
1411
|
+
delta.time_part()
|
|
1412
|
+
)
|
|
1413
|
+
case None:
|
|
1414
|
+
return None
|
|
1415
|
+
case never:
|
|
1416
|
+
assert_never(never)
|
|
416
1417
|
|
|
417
1418
|
|
|
418
1419
|
@dataclass(kw_only=True, slots=True)
|
|
419
|
-
class
|
|
1420
|
+
class ToPyTimeDeltaError(Exception):
|
|
420
1421
|
nanoseconds: int
|
|
421
1422
|
|
|
422
1423
|
@override
|
|
423
1424
|
def __str__(self) -> str:
|
|
424
|
-
return f"
|
|
1425
|
+
return f"Time delta must not contain nanoseconds; got {self.nanoseconds}"
|
|
425
1426
|
|
|
426
1427
|
|
|
427
1428
|
##
|
|
428
1429
|
|
|
429
1430
|
|
|
430
|
-
def
|
|
431
|
-
"""
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1431
|
+
def to_seconds(delta: Delta, /) -> int:
|
|
1432
|
+
"""Compute the number of seconds in a delta."""
|
|
1433
|
+
match delta:
|
|
1434
|
+
case DateDelta():
|
|
1435
|
+
try:
|
|
1436
|
+
days = to_days(delta)
|
|
1437
|
+
except _ToDaysMonthsError as error:
|
|
1438
|
+
raise _ToSecondsMonthsError(delta=delta, months=error.months) from None
|
|
1439
|
+
return 24 * 60 * 60 * days
|
|
1440
|
+
case TimeDelta():
|
|
1441
|
+
nanos = to_nanoseconds(delta)
|
|
1442
|
+
seconds, remainder = divmod(nanos, int(1e9))
|
|
1443
|
+
if remainder != 0:
|
|
1444
|
+
raise _ToSecondsNanosecondsError(delta=delta, nanoseconds=remainder)
|
|
1445
|
+
return seconds
|
|
1446
|
+
case DateTimeDelta():
|
|
1447
|
+
try:
|
|
1448
|
+
return to_seconds(delta.date_part()) + to_seconds(delta.time_part())
|
|
1449
|
+
except _ToSecondsMonthsError as error:
|
|
1450
|
+
raise _ToSecondsMonthsError(delta=delta, months=error.months) from None
|
|
1451
|
+
except _ToSecondsNanosecondsError as error:
|
|
1452
|
+
raise _ToSecondsNanosecondsError(
|
|
1453
|
+
delta=delta, nanoseconds=error.nanoseconds
|
|
1454
|
+
) from None
|
|
1455
|
+
case never:
|
|
1456
|
+
assert_never(never)
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
@dataclass(kw_only=True, slots=True)
|
|
1460
|
+
class ToSecondsError(Exception): ...
|
|
437
1461
|
|
|
438
1462
|
|
|
439
1463
|
@dataclass(kw_only=True, slots=True)
|
|
440
|
-
class
|
|
441
|
-
|
|
1464
|
+
class _ToSecondsMonthsError(ToSecondsError):
|
|
1465
|
+
delta: DateOrDateTimeDelta
|
|
1466
|
+
months: int
|
|
442
1467
|
|
|
443
1468
|
@override
|
|
444
1469
|
def __str__(self) -> str:
|
|
445
|
-
return f"
|
|
1470
|
+
return f"Delta must not contain months; got {self.months}"
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
@dataclass(kw_only=True, slots=True)
|
|
1474
|
+
class _ToSecondsNanosecondsError(ToSecondsError):
|
|
1475
|
+
delta: TimeOrDateTimeDelta
|
|
1476
|
+
nanoseconds: int
|
|
1477
|
+
|
|
1478
|
+
@override
|
|
1479
|
+
def __str__(self) -> str:
|
|
1480
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
446
1481
|
|
|
447
1482
|
|
|
448
1483
|
##
|
|
449
1484
|
|
|
450
1485
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1486
|
+
@overload
|
|
1487
|
+
def to_time(time: Sentinel, /, *, time_zone: TimeZoneLike = UTC) -> Sentinel: ...
|
|
1488
|
+
@overload
|
|
1489
|
+
def to_time(
|
|
1490
|
+
time: MaybeCallableTimeLike | None | dt.time = get_time,
|
|
1491
|
+
/,
|
|
1492
|
+
*,
|
|
1493
|
+
time_zone: TimeZoneLike = UTC,
|
|
1494
|
+
) -> Time: ...
|
|
1495
|
+
def to_time(
|
|
1496
|
+
time: MaybeCallableTimeLike | dt.time | None | Sentinel = get_time,
|
|
1497
|
+
/,
|
|
1498
|
+
*,
|
|
1499
|
+
time_zone: TimeZoneLike = UTC,
|
|
1500
|
+
) -> Time | Sentinel:
|
|
1501
|
+
"""Convert to a time."""
|
|
1502
|
+
match time:
|
|
1503
|
+
case Time() | Sentinel():
|
|
1504
|
+
return time
|
|
1505
|
+
case None:
|
|
1506
|
+
return get_time(time_zone)
|
|
1507
|
+
case str():
|
|
1508
|
+
return Time.parse_iso(time)
|
|
1509
|
+
case dt.time():
|
|
1510
|
+
return Time.from_py_time(time)
|
|
1511
|
+
case Callable() as func:
|
|
1512
|
+
return to_time(func(), time_zone=time_zone)
|
|
1513
|
+
case never:
|
|
1514
|
+
assert_never(never)
|
|
455
1515
|
|
|
456
1516
|
|
|
457
1517
|
##
|
|
458
1518
|
|
|
459
1519
|
|
|
460
|
-
def
|
|
461
|
-
"""
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1520
|
+
def to_time_delta(nanos: int, /) -> TimeDelta:
|
|
1521
|
+
"""Construct a time delta."""
|
|
1522
|
+
components = _to_time_delta_components(nanos)
|
|
1523
|
+
return TimeDelta(
|
|
1524
|
+
hours=components.hours,
|
|
1525
|
+
minutes=components.minutes,
|
|
1526
|
+
seconds=components.seconds,
|
|
1527
|
+
microseconds=components.microseconds,
|
|
1528
|
+
milliseconds=components.milliseconds,
|
|
1529
|
+
nanoseconds=components.nanoseconds,
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
@dataclass(kw_only=True, slots=True)
|
|
1534
|
+
class _TimeDeltaComponents:
|
|
1535
|
+
hours: int
|
|
1536
|
+
minutes: int
|
|
1537
|
+
seconds: int
|
|
1538
|
+
microseconds: int
|
|
1539
|
+
milliseconds: int
|
|
1540
|
+
nanoseconds: int
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
def _to_time_delta_components(nanos: int, /) -> _TimeDeltaComponents:
|
|
1544
|
+
sign_use = sign(nanos)
|
|
1545
|
+
micros, nanos = divmod(nanos, int(1e3))
|
|
1546
|
+
millis, micros = divmod(micros, int(1e3))
|
|
1547
|
+
secs, millis = divmod(millis, int(1e3))
|
|
1548
|
+
mins, secs = divmod(secs, 60)
|
|
1549
|
+
hours, mins = divmod(mins, 60)
|
|
1550
|
+
match sign_use: # pragma: no cover
|
|
1551
|
+
case 1:
|
|
1552
|
+
if nanos < 0:
|
|
1553
|
+
nanos += int(1e3)
|
|
1554
|
+
micros -= 1
|
|
1555
|
+
if micros < 0:
|
|
1556
|
+
micros += int(1e3)
|
|
1557
|
+
millis -= 1
|
|
1558
|
+
if millis < 0:
|
|
1559
|
+
millis += int(1e3)
|
|
1560
|
+
secs -= 1
|
|
1561
|
+
if secs < 0:
|
|
1562
|
+
secs += 60
|
|
1563
|
+
mins -= 1
|
|
1564
|
+
if mins < 0:
|
|
1565
|
+
mins += 60
|
|
1566
|
+
hours -= 1
|
|
1567
|
+
case -1:
|
|
1568
|
+
if nanos > 0:
|
|
1569
|
+
nanos -= int(1e3)
|
|
1570
|
+
micros += 1
|
|
1571
|
+
if micros > 0:
|
|
1572
|
+
micros -= int(1e3)
|
|
1573
|
+
millis += 1
|
|
1574
|
+
if millis > 0:
|
|
1575
|
+
millis -= int(1e3)
|
|
1576
|
+
secs += 1
|
|
1577
|
+
if secs > 0:
|
|
1578
|
+
secs -= 60
|
|
1579
|
+
mins += 1
|
|
1580
|
+
if mins > 0:
|
|
1581
|
+
mins -= 60
|
|
1582
|
+
hours += 1
|
|
1583
|
+
case 0:
|
|
1584
|
+
...
|
|
1585
|
+
return _TimeDeltaComponents(
|
|
1586
|
+
hours=hours,
|
|
1587
|
+
minutes=mins,
|
|
1588
|
+
seconds=secs,
|
|
1589
|
+
microseconds=micros,
|
|
1590
|
+
milliseconds=millis,
|
|
1591
|
+
nanoseconds=nanos,
|
|
1592
|
+
)
|
|
466
1593
|
|
|
467
1594
|
|
|
468
1595
|
##
|
|
469
1596
|
|
|
470
1597
|
|
|
471
|
-
def
|
|
472
|
-
"""
|
|
473
|
-
if isinstance(duration, int | float):
|
|
474
|
-
return str(duration)
|
|
1598
|
+
def to_weeks(delta: Delta, /) -> int:
|
|
1599
|
+
"""Compute the number of weeks in a delta."""
|
|
475
1600
|
try:
|
|
476
|
-
|
|
477
|
-
except
|
|
478
|
-
raise
|
|
1601
|
+
days = to_days(delta)
|
|
1602
|
+
except _ToDaysMonthsError as error:
|
|
1603
|
+
raise _ToWeeksMonthsError(delta=error.delta, months=error.months) from None
|
|
1604
|
+
except _ToDaysNanosecondsError as error:
|
|
1605
|
+
raise _ToWeeksNanosecondsError(
|
|
1606
|
+
delta=error.delta, nanoseconds=error.nanoseconds
|
|
1607
|
+
) from None
|
|
1608
|
+
weeks, remainder = divmod(days, 7)
|
|
1609
|
+
if remainder != 0:
|
|
1610
|
+
raise _ToWeeksDaysError(delta=delta, days=remainder) from None
|
|
1611
|
+
return weeks
|
|
479
1612
|
|
|
480
1613
|
|
|
481
1614
|
@dataclass(kw_only=True, slots=True)
|
|
482
|
-
class
|
|
483
|
-
|
|
1615
|
+
class ToWeeksError(Exception): ...
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
@dataclass(kw_only=True, slots=True)
|
|
1619
|
+
class _ToWeeksMonthsError(ToWeeksError):
|
|
1620
|
+
delta: DateOrDateTimeDelta
|
|
1621
|
+
months: int
|
|
484
1622
|
|
|
485
1623
|
@override
|
|
486
1624
|
def __str__(self) -> str:
|
|
487
|
-
return f"
|
|
1625
|
+
return f"Delta must not contain months; got {self.months}"
|
|
488
1626
|
|
|
489
1627
|
|
|
490
|
-
|
|
491
|
-
|
|
1628
|
+
@dataclass(kw_only=True, slots=True)
|
|
1629
|
+
class _ToWeeksNanosecondsError(ToWeeksError):
|
|
1630
|
+
delta: TimeOrDateTimeDelta
|
|
1631
|
+
nanoseconds: int
|
|
492
1632
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
pdt = PlainDateTime.from_py_datetime(datetime)
|
|
497
|
-
except ValueError:
|
|
498
|
-
raise SerializePlainDateTimeError(datetime=datetime) from None
|
|
499
|
-
return pdt.format_common_iso()
|
|
1633
|
+
@override
|
|
1634
|
+
def __str__(self) -> str:
|
|
1635
|
+
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
500
1636
|
|
|
501
1637
|
|
|
502
1638
|
@dataclass(kw_only=True, slots=True)
|
|
503
|
-
class
|
|
504
|
-
|
|
1639
|
+
class _ToWeeksDaysError(ToWeeksError):
|
|
1640
|
+
delta: Delta
|
|
1641
|
+
days: int
|
|
505
1642
|
|
|
506
1643
|
@override
|
|
507
1644
|
def __str__(self) -> str:
|
|
508
|
-
return f"
|
|
1645
|
+
return f"Delta must not contain extra days; got {self.days}"
|
|
509
1646
|
|
|
510
1647
|
|
|
511
1648
|
##
|
|
512
1649
|
|
|
513
1650
|
|
|
514
|
-
def
|
|
515
|
-
"""
|
|
516
|
-
|
|
1651
|
+
def to_years(delta: DateOrDateTimeDelta, /) -> int:
|
|
1652
|
+
"""Compute the number of years in a delta."""
|
|
1653
|
+
match delta:
|
|
1654
|
+
case DateDelta():
|
|
1655
|
+
years, months, days = delta.in_years_months_days()
|
|
1656
|
+
if months != 0:
|
|
1657
|
+
raise _ToYearsMonthsError(delta=delta, months=months)
|
|
1658
|
+
if days != 0:
|
|
1659
|
+
raise _ToYearsDaysError(delta=delta, days=days)
|
|
1660
|
+
return years
|
|
1661
|
+
case DateTimeDelta():
|
|
1662
|
+
if delta.time_part() != TimeDelta():
|
|
1663
|
+
raise _ToYearsTimeError(delta=delta)
|
|
1664
|
+
try:
|
|
1665
|
+
return to_years(delta.date_part())
|
|
1666
|
+
except _ToYearsMonthsError as error:
|
|
1667
|
+
raise _ToYearsMonthsError(delta=delta, months=error.months) from None
|
|
1668
|
+
except _ToYearsDaysError as error:
|
|
1669
|
+
raise _ToYearsDaysError(delta=delta, days=error.days) from None
|
|
1670
|
+
case never:
|
|
1671
|
+
assert_never(never)
|
|
517
1672
|
|
|
518
1673
|
|
|
519
|
-
|
|
1674
|
+
@dataclass(kw_only=True, slots=True)
|
|
1675
|
+
class ToYearsError(Exception): ...
|
|
520
1676
|
|
|
521
1677
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1678
|
+
@dataclass(kw_only=True, slots=True)
|
|
1679
|
+
class _ToYearsMonthsError(ToYearsError):
|
|
1680
|
+
delta: DateOrDateTimeDelta
|
|
1681
|
+
months: int
|
|
1682
|
+
|
|
1683
|
+
@override
|
|
1684
|
+
def __str__(self) -> str:
|
|
1685
|
+
return f"Delta must not contain months; got {self.months}"
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
@dataclass(kw_only=True, slots=True)
|
|
1689
|
+
class _ToYearsDaysError(ToYearsError):
|
|
1690
|
+
delta: DateOrDateTimeDelta
|
|
1691
|
+
days: int
|
|
1692
|
+
|
|
1693
|
+
@override
|
|
1694
|
+
def __str__(self) -> str:
|
|
1695
|
+
return f"Delta must not contain days; got {self.days}"
|
|
529
1696
|
|
|
530
1697
|
|
|
531
1698
|
@dataclass(kw_only=True, slots=True)
|
|
532
|
-
class
|
|
533
|
-
|
|
1699
|
+
class _ToYearsTimeError(ToYearsError):
|
|
1700
|
+
delta: DateTimeDelta
|
|
534
1701
|
|
|
535
1702
|
@override
|
|
536
1703
|
def __str__(self) -> str:
|
|
537
|
-
return f"
|
|
1704
|
+
return f"Delta must not contain a time part; got {self.delta.time_part()}"
|
|
538
1705
|
|
|
539
1706
|
|
|
540
1707
|
##
|
|
541
1708
|
|
|
542
1709
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1710
|
+
@overload
|
|
1711
|
+
def to_zoned_date_time(
|
|
1712
|
+
date_time: Sentinel, /, *, time_zone: TimeZoneLike | None = None
|
|
1713
|
+
) -> Sentinel: ...
|
|
1714
|
+
@overload
|
|
1715
|
+
def to_zoned_date_time(
|
|
1716
|
+
date_time: MaybeCallableZonedDateTimeLike | dt.datetime | None = get_now,
|
|
1717
|
+
/,
|
|
1718
|
+
*,
|
|
1719
|
+
time_zone: TimeZoneLike | None = None,
|
|
1720
|
+
) -> ZonedDateTime: ...
|
|
1721
|
+
def to_zoned_date_time(
|
|
1722
|
+
date_time: MaybeCallableZonedDateTimeLike | dt.datetime | None | Sentinel = get_now,
|
|
1723
|
+
/,
|
|
1724
|
+
*,
|
|
1725
|
+
time_zone: TimeZoneLike | None = None,
|
|
1726
|
+
) -> ZonedDateTime | Sentinel:
|
|
1727
|
+
"""Convert to a zoned date-time."""
|
|
1728
|
+
match date_time:
|
|
1729
|
+
case ZonedDateTime() as date_time_use:
|
|
1730
|
+
...
|
|
1731
|
+
case Sentinel():
|
|
1732
|
+
return sentinel
|
|
1733
|
+
case None:
|
|
1734
|
+
return get_now(UTC if time_zone is None else time_zone)
|
|
1735
|
+
case str() as text:
|
|
1736
|
+
date_time_use = ZonedDateTime.parse_iso(text.replace("~", "/"))
|
|
1737
|
+
case dt.datetime() as py_date_time:
|
|
1738
|
+
if isinstance(date_time.tzinfo, ZoneInfo):
|
|
1739
|
+
py_date_time_use = py_date_time
|
|
1740
|
+
elif date_time.tzinfo is dt.UTC:
|
|
1741
|
+
py_date_time_use = py_date_time.astimezone(UTC)
|
|
1742
|
+
else:
|
|
1743
|
+
raise ToZonedDateTimeError(date_time=date_time)
|
|
1744
|
+
date_time_use = ZonedDateTime.from_py_datetime(py_date_time_use)
|
|
1745
|
+
case Callable() as func:
|
|
1746
|
+
return to_zoned_date_time(func(), time_zone=time_zone)
|
|
1747
|
+
case never:
|
|
1748
|
+
assert_never(never)
|
|
1749
|
+
if time_zone is None:
|
|
1750
|
+
return date_time_use
|
|
1751
|
+
return date_time_use.to_tz(to_time_zone_name(time_zone))
|
|
554
1752
|
|
|
555
1753
|
|
|
556
1754
|
@dataclass(kw_only=True, slots=True)
|
|
557
|
-
class
|
|
558
|
-
|
|
1755
|
+
class ToZonedDateTimeError(Exception):
|
|
1756
|
+
date_time: dt.datetime
|
|
559
1757
|
|
|
560
1758
|
@override
|
|
561
1759
|
def __str__(self) -> str:
|
|
562
|
-
return f"
|
|
1760
|
+
return f"Expected date-time to have a `ZoneInfo` or `dt.UTC` as its timezone; got {self.date_time.tzinfo}"
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
##
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def two_digit_year_month(year: int, month: int, /) -> YearMonth:
|
|
1767
|
+
"""Construct a year-month from a 2-digit year."""
|
|
1768
|
+
min_year = DATE_TWO_DIGIT_YEAR_MIN.year
|
|
1769
|
+
max_year = DATE_TWO_DIGIT_YEAR_MAX.year
|
|
1770
|
+
years = range(min_year, max_year + 1)
|
|
1771
|
+
(year_use,) = (y for y in years if y % 100 == year)
|
|
1772
|
+
return YearMonth(year_use, month)
|
|
563
1773
|
|
|
564
1774
|
|
|
565
1775
|
##
|
|
@@ -587,112 +1797,292 @@ class WheneverLogRecord(LogRecord):
|
|
|
587
1797
|
name, level, pathname, lineno, msg, args, exc_info, func, sinfo
|
|
588
1798
|
)
|
|
589
1799
|
length = self._get_length()
|
|
590
|
-
plain = format(
|
|
591
|
-
|
|
592
|
-
self.zoned_datetime = f"{plain}[{time_zone}]"
|
|
593
|
-
|
|
594
|
-
@classmethod
|
|
595
|
-
@cache
|
|
596
|
-
def _get_time_zone(cls) -> ZoneInfo:
|
|
597
|
-
"""Get the local timezone."""
|
|
598
|
-
try:
|
|
599
|
-
from utilities.tzlocal import get_local_time_zone
|
|
600
|
-
except ModuleNotFoundError: # pragma: no cover
|
|
601
|
-
return UTC
|
|
602
|
-
return get_local_time_zone()
|
|
603
|
-
|
|
604
|
-
@classmethod
|
|
605
|
-
@cache
|
|
606
|
-
def _get_time_zone_key(cls) -> str:
|
|
607
|
-
"""Get the local timezone as a string."""
|
|
608
|
-
return cls._get_time_zone().key
|
|
1800
|
+
plain = format(get_now_local().to_plain().format_iso(), f"{length}s")
|
|
1801
|
+
self.zoned_datetime = f"{plain}[{LOCAL_TIME_ZONE_NAME}]"
|
|
609
1802
|
|
|
610
1803
|
@classmethod
|
|
611
1804
|
@cache
|
|
612
1805
|
def _get_length(cls) -> int:
|
|
613
1806
|
"""Get maximum length of a formatted string."""
|
|
614
|
-
now =
|
|
615
|
-
return len(now.
|
|
1807
|
+
now = get_now_local().replace(nanosecond=1000).to_plain()
|
|
1808
|
+
return len(now.format_iso())
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
##
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
1815
|
+
class ZonedDateTimePeriod:
|
|
1816
|
+
"""A period of time."""
|
|
1817
|
+
|
|
1818
|
+
start: ZonedDateTime
|
|
1819
|
+
end: ZonedDateTime
|
|
1820
|
+
|
|
1821
|
+
def __post_init__(self) -> None:
|
|
1822
|
+
if self.start > self.end:
|
|
1823
|
+
raise _ZonedDateTimePeriodInvalidError(start=self.start, end=self.end)
|
|
1824
|
+
if self.start.tz != self.end.tz:
|
|
1825
|
+
raise _ZonedDateTimePeriodTimeZoneError(
|
|
1826
|
+
start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
def __add__(self, other: TimeDelta, /) -> Self:
|
|
1830
|
+
"""Offset the period."""
|
|
1831
|
+
return self.replace(start=self.start + other, end=self.end + other)
|
|
1832
|
+
|
|
1833
|
+
def __contains__(self, other: ZonedDateTime, /) -> bool:
|
|
1834
|
+
"""Check if a date/datetime lies in the period."""
|
|
1835
|
+
return self.start <= other <= self.end
|
|
1836
|
+
|
|
1837
|
+
@override
|
|
1838
|
+
def __repr__(self) -> str:
|
|
1839
|
+
cls = get_class_name(self)
|
|
1840
|
+
return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
|
|
1841
|
+
|
|
1842
|
+
def __sub__(self, other: TimeDelta, /) -> Self:
|
|
1843
|
+
"""Offset the period."""
|
|
1844
|
+
return self.replace(start=self.start - other, end=self.end - other)
|
|
1845
|
+
|
|
1846
|
+
@property
|
|
1847
|
+
def delta(self) -> TimeDelta:
|
|
1848
|
+
"""The duration of the period."""
|
|
1849
|
+
return self.end - self.start
|
|
1850
|
+
|
|
1851
|
+
@overload
|
|
1852
|
+
def exact_eq(self, period: ZonedDateTimePeriod, /) -> bool: ...
|
|
1853
|
+
@overload
|
|
1854
|
+
def exact_eq(self, start: ZonedDateTime, end: ZonedDateTime, /) -> bool: ...
|
|
1855
|
+
@overload
|
|
1856
|
+
def exact_eq(
|
|
1857
|
+
self, start: PlainDateTime, end: PlainDateTime, time_zone: ZoneInfo, /
|
|
1858
|
+
) -> bool: ...
|
|
1859
|
+
def exact_eq(self, *args: Any) -> bool:
|
|
1860
|
+
"""Check if a period is exactly equal to another."""
|
|
1861
|
+
if (len(args) == 1) and isinstance(args[0], ZonedDateTimePeriod):
|
|
1862
|
+
return self.start.exact_eq(args[0].start) and self.end.exact_eq(args[0].end)
|
|
1863
|
+
if (
|
|
1864
|
+
(len(args) == 2)
|
|
1865
|
+
and isinstance(args[0], ZonedDateTime)
|
|
1866
|
+
and isinstance(args[1], ZonedDateTime)
|
|
1867
|
+
):
|
|
1868
|
+
return self.exact_eq(ZonedDateTimePeriod(args[0], args[1]))
|
|
1869
|
+
if (
|
|
1870
|
+
(len(args) == 3)
|
|
1871
|
+
and isinstance(args[0], PlainDateTime)
|
|
1872
|
+
and isinstance(args[1], PlainDateTime)
|
|
1873
|
+
and isinstance(args[2], ZoneInfo)
|
|
1874
|
+
):
|
|
1875
|
+
return self.exact_eq(
|
|
1876
|
+
ZonedDateTimePeriod(
|
|
1877
|
+
args[0].assume_tz(args[2].key), args[1].assume_tz(args[2].key)
|
|
1878
|
+
)
|
|
1879
|
+
)
|
|
1880
|
+
raise _ZonedDateTimePeriodExactEqError(args=args)
|
|
1881
|
+
|
|
1882
|
+
def format_compact(self) -> str:
|
|
1883
|
+
"""Format the period in a compact fashion."""
|
|
1884
|
+
fc, start, end = format_compact, self.start, self.end
|
|
1885
|
+
if start == end:
|
|
1886
|
+
if end.second != 0:
|
|
1887
|
+
return f"{fc(start)}="
|
|
1888
|
+
if end.minute != 0:
|
|
1889
|
+
return f"{fc(start, fmt='%Y%m%dT%H%M')}="
|
|
1890
|
+
return f"{fc(start, fmt='%Y%m%dT%H')}="
|
|
1891
|
+
if start.date() == end.date():
|
|
1892
|
+
if end.second != 0:
|
|
1893
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M%S')}"
|
|
1894
|
+
if end.minute != 0:
|
|
1895
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M')}"
|
|
1896
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%H')}"
|
|
1897
|
+
if start.date().year_month() == end.date().year_month():
|
|
1898
|
+
if end.second != 0:
|
|
1899
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M%S')}"
|
|
1900
|
+
if end.minute != 0:
|
|
1901
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M')}"
|
|
1902
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H')}"
|
|
1903
|
+
if start.year == end.year:
|
|
1904
|
+
if end.second != 0:
|
|
1905
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M%S')}"
|
|
1906
|
+
if end.minute != 0:
|
|
1907
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M')}"
|
|
1908
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H')}"
|
|
1909
|
+
if end.second != 0:
|
|
1910
|
+
return f"{fc(start.to_plain())}-{fc(end)}"
|
|
1911
|
+
if end.minute != 0:
|
|
1912
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H%M')}"
|
|
1913
|
+
return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H')}"
|
|
616
1914
|
|
|
617
1915
|
@classmethod
|
|
618
|
-
def
|
|
619
|
-
|
|
620
|
-
|
|
1916
|
+
def from_dict(
|
|
1917
|
+
cls, mapping: PeriodDict[ZonedDateTime] | PeriodDict[dt.datetime], /
|
|
1918
|
+
) -> Self:
|
|
1919
|
+
"""Convert the dictionary to a period."""
|
|
1920
|
+
match mapping["start"]:
|
|
1921
|
+
case ZonedDateTime() as start:
|
|
1922
|
+
...
|
|
1923
|
+
case dt.date() as py_datetime:
|
|
1924
|
+
start = ZonedDateTime.from_py_datetime(py_datetime)
|
|
1925
|
+
case never:
|
|
1926
|
+
assert_never(never)
|
|
1927
|
+
match mapping["end"]:
|
|
1928
|
+
case ZonedDateTime() as end:
|
|
1929
|
+
...
|
|
1930
|
+
case dt.date() as py_datetime:
|
|
1931
|
+
end = ZonedDateTime.from_py_datetime(py_datetime)
|
|
1932
|
+
case never:
|
|
1933
|
+
assert_never(never)
|
|
1934
|
+
return cls(start=start, end=end)
|
|
1935
|
+
|
|
1936
|
+
def replace(
|
|
1937
|
+
self,
|
|
1938
|
+
*,
|
|
1939
|
+
start: ZonedDateTime | Sentinel = sentinel,
|
|
1940
|
+
end: ZonedDateTime | Sentinel = sentinel,
|
|
1941
|
+
) -> Self:
|
|
1942
|
+
"""Replace elements of the period."""
|
|
1943
|
+
return replace_non_sentinel(self, start=start, end=end)
|
|
621
1944
|
|
|
1945
|
+
@property
|
|
1946
|
+
def time_zone(self) -> ZoneInfo:
|
|
1947
|
+
"""The time zone of the period."""
|
|
1948
|
+
return ZoneInfo(self.start.tz)
|
|
622
1949
|
|
|
623
|
-
|
|
1950
|
+
def to_dict(self) -> PeriodDict[ZonedDateTime]:
|
|
1951
|
+
"""Convert the period to a dictionary."""
|
|
1952
|
+
return PeriodDict(start=self.start, end=self.end)
|
|
624
1953
|
|
|
1954
|
+
def to_py_dict(self) -> PeriodDict[dt.datetime]:
|
|
1955
|
+
"""Convert the period to a dictionary."""
|
|
1956
|
+
return PeriodDict(start=self.start.py_datetime(), end=self.end.py_datetime())
|
|
625
1957
|
|
|
626
|
-
def
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1958
|
+
def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
|
|
1959
|
+
"""Convert the time zone."""
|
|
1960
|
+
tz = to_time_zone_name(time_zone)
|
|
1961
|
+
return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
@dataclass(kw_only=True, slots=True)
|
|
1965
|
+
class ZonedDateTimePeriodError(Exception): ...
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
@dataclass(kw_only=True, slots=True)
|
|
1969
|
+
class _ZonedDateTimePeriodInvalidError[T: Date | ZonedDateTime](
|
|
1970
|
+
ZonedDateTimePeriodError
|
|
1971
|
+
):
|
|
1972
|
+
start: T
|
|
1973
|
+
end: T
|
|
1974
|
+
|
|
1975
|
+
@override
|
|
1976
|
+
def __str__(self) -> str:
|
|
1977
|
+
return f"Invalid period; got {self.start} > {self.end}"
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
@dataclass(kw_only=True, slots=True)
|
|
1981
|
+
class _ZonedDateTimePeriodTimeZoneError(ZonedDateTimePeriodError):
|
|
1982
|
+
start: ZoneInfo
|
|
1983
|
+
end: ZoneInfo
|
|
1984
|
+
|
|
1985
|
+
@override
|
|
1986
|
+
def __str__(self) -> str:
|
|
1987
|
+
return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
|
|
643
1988
|
|
|
644
1989
|
|
|
645
1990
|
@dataclass(kw_only=True, slots=True)
|
|
646
|
-
class
|
|
647
|
-
|
|
1991
|
+
class _ZonedDateTimePeriodExactEqError(ZonedDateTimePeriodError):
|
|
1992
|
+
args: tuple[Any, ...]
|
|
648
1993
|
|
|
649
1994
|
@override
|
|
650
1995
|
def __str__(self) -> str:
|
|
651
|
-
return f"
|
|
1996
|
+
return f"Invalid arguments; got {self.args}"
|
|
652
1997
|
|
|
653
1998
|
|
|
654
1999
|
__all__ = [
|
|
655
|
-
"
|
|
656
|
-
"
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
"
|
|
660
|
-
"
|
|
661
|
-
"
|
|
662
|
-
"
|
|
663
|
-
"
|
|
664
|
-
"
|
|
665
|
-
"
|
|
666
|
-
"
|
|
667
|
-
"
|
|
668
|
-
"
|
|
669
|
-
"
|
|
670
|
-
"
|
|
671
|
-
"
|
|
672
|
-
"
|
|
673
|
-
"
|
|
674
|
-
"
|
|
2000
|
+
"DATE_DELTA_MAX",
|
|
2001
|
+
"DATE_DELTA_MIN",
|
|
2002
|
+
"DATE_DELTA_PARSABLE_MAX",
|
|
2003
|
+
"DATE_DELTA_PARSABLE_MIN",
|
|
2004
|
+
"DATE_TIME_DELTA_MAX",
|
|
2005
|
+
"DATE_TIME_DELTA_MIN",
|
|
2006
|
+
"DATE_TIME_DELTA_PARSABLE_MAX",
|
|
2007
|
+
"DATE_TIME_DELTA_PARSABLE_MIN",
|
|
2008
|
+
"DATE_TWO_DIGIT_YEAR_MAX",
|
|
2009
|
+
"DATE_TWO_DIGIT_YEAR_MIN",
|
|
2010
|
+
"DAY",
|
|
2011
|
+
"HOUR",
|
|
2012
|
+
"MICROSECOND",
|
|
2013
|
+
"MILLISECOND",
|
|
2014
|
+
"MINUTE",
|
|
2015
|
+
"MONTH",
|
|
2016
|
+
"NOW_LOCAL",
|
|
2017
|
+
"NOW_LOCAL_PLAIN",
|
|
2018
|
+
"NOW_PLAIN",
|
|
2019
|
+
"SECOND",
|
|
2020
|
+
"TIME_DELTA_MAX",
|
|
2021
|
+
"TIME_DELTA_MIN",
|
|
2022
|
+
"TIME_LOCAL",
|
|
2023
|
+
"TIME_UTC",
|
|
2024
|
+
"TODAY_LOCAL",
|
|
2025
|
+
"TODAY_UTC",
|
|
2026
|
+
"WEEK",
|
|
2027
|
+
"YEAR",
|
|
2028
|
+
"ZERO_DAYS",
|
|
2029
|
+
"ZERO_TIME",
|
|
2030
|
+
"ZONED_DATE_TIME_MAX",
|
|
2031
|
+
"ZONED_DATE_TIME_MIN",
|
|
2032
|
+
"DatePeriod",
|
|
2033
|
+
"DatePeriodError",
|
|
2034
|
+
"MeanDateTimeError",
|
|
2035
|
+
"MinMaxDateError",
|
|
2036
|
+
"PeriodDict",
|
|
2037
|
+
"RoundDateOrDateTimeError",
|
|
2038
|
+
"TimePeriod",
|
|
2039
|
+
"ToDaysError",
|
|
2040
|
+
"ToMinutesError",
|
|
2041
|
+
"ToMonthsAndDaysError",
|
|
2042
|
+
"ToMonthsError",
|
|
2043
|
+
"ToNanosecondsError",
|
|
2044
|
+
"ToPyTimeDeltaError",
|
|
2045
|
+
"ToSecondsError",
|
|
2046
|
+
"ToWeeksError",
|
|
2047
|
+
"ToYearsError",
|
|
675
2048
|
"WheneverLogRecord",
|
|
676
|
-
"
|
|
677
|
-
"
|
|
678
|
-
"
|
|
679
|
-
"
|
|
680
|
-
"
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
"
|
|
684
|
-
"
|
|
685
|
-
"
|
|
686
|
-
"
|
|
687
|
-
"
|
|
688
|
-
"
|
|
689
|
-
"
|
|
690
|
-
"
|
|
691
|
-
"
|
|
692
|
-
"
|
|
693
|
-
"
|
|
694
|
-
"
|
|
695
|
-
"
|
|
696
|
-
"
|
|
697
|
-
"
|
|
2049
|
+
"ZonedDateTimePeriod",
|
|
2050
|
+
"ZonedDateTimePeriodError",
|
|
2051
|
+
"add_year_month",
|
|
2052
|
+
"datetime_utc",
|
|
2053
|
+
"diff_year_month",
|
|
2054
|
+
"format_compact",
|
|
2055
|
+
"from_timestamp",
|
|
2056
|
+
"from_timestamp_millis",
|
|
2057
|
+
"from_timestamp_nanos",
|
|
2058
|
+
"get_now",
|
|
2059
|
+
"get_now_local",
|
|
2060
|
+
"get_now_local_plain",
|
|
2061
|
+
"get_now_plain",
|
|
2062
|
+
"get_time",
|
|
2063
|
+
"get_time_local",
|
|
2064
|
+
"get_today",
|
|
2065
|
+
"get_today_local",
|
|
2066
|
+
"is_weekend",
|
|
2067
|
+
"mean_datetime",
|
|
2068
|
+
"min_max_date",
|
|
2069
|
+
"round_date_or_date_time",
|
|
2070
|
+
"sub_year_month",
|
|
2071
|
+
"to_date",
|
|
2072
|
+
"to_date_time_delta",
|
|
2073
|
+
"to_days",
|
|
2074
|
+
"to_microseconds",
|
|
2075
|
+
"to_milliseconds",
|
|
2076
|
+
"to_minutes",
|
|
2077
|
+
"to_months",
|
|
2078
|
+
"to_months_and_days",
|
|
2079
|
+
"to_nanoseconds",
|
|
2080
|
+
"to_py_date_or_date_time",
|
|
2081
|
+
"to_py_time_delta",
|
|
2082
|
+
"to_seconds",
|
|
2083
|
+
"to_time",
|
|
2084
|
+
"to_weeks",
|
|
2085
|
+
"to_years",
|
|
2086
|
+
"to_zoned_date_time",
|
|
2087
|
+
"two_digit_year_month",
|
|
698
2088
|
]
|