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/datetime.py
DELETED
|
@@ -1,1409 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import datetime as dt
|
|
4
|
-
from collections.abc import Callable, Iterable, Sequence
|
|
5
|
-
from dataclasses import dataclass, replace
|
|
6
|
-
from re import search, sub
|
|
7
|
-
from statistics import fmean
|
|
8
|
-
from typing import (
|
|
9
|
-
TYPE_CHECKING,
|
|
10
|
-
Any,
|
|
11
|
-
Literal,
|
|
12
|
-
Self,
|
|
13
|
-
SupportsFloat,
|
|
14
|
-
TypeGuard,
|
|
15
|
-
assert_never,
|
|
16
|
-
overload,
|
|
17
|
-
override,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from utilities.iterables import OneEmptyError, one
|
|
21
|
-
from utilities.math import SafeRoundError, round_, safe_round
|
|
22
|
-
from utilities.platform import SYSTEM
|
|
23
|
-
from utilities.sentinel import Sentinel, sentinel
|
|
24
|
-
from utilities.types import MaybeStr
|
|
25
|
-
from utilities.typing import is_instance_gen
|
|
26
|
-
from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
|
|
27
|
-
|
|
28
|
-
if TYPE_CHECKING:
|
|
29
|
-
from collections.abc import Iterator
|
|
30
|
-
|
|
31
|
-
from utilities.types import (
|
|
32
|
-
DateOrDateTime,
|
|
33
|
-
Duration,
|
|
34
|
-
MaybeCallableDate,
|
|
35
|
-
MaybeCallableDateTime,
|
|
36
|
-
RoundMode,
|
|
37
|
-
TimeZoneLike,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
_DAYS_PER_YEAR = 365.25
|
|
42
|
-
_MICROSECONDS_PER_MILLISECOND = int(1e3)
|
|
43
|
-
_MICROSECONDS_PER_SECOND = int(1e6)
|
|
44
|
-
_SECONDS_PER_DAY = 24 * 60 * 60
|
|
45
|
-
_MICROSECONDS_PER_DAY = _MICROSECONDS_PER_SECOND * _SECONDS_PER_DAY
|
|
46
|
-
DATETIME_MIN_UTC = dt.datetime.min.replace(tzinfo=UTC)
|
|
47
|
-
DATETIME_MAX_UTC = dt.datetime.max.replace(tzinfo=UTC)
|
|
48
|
-
DATETIME_MIN_NAIVE = DATETIME_MIN_UTC.replace(tzinfo=None)
|
|
49
|
-
DATETIME_MAX_NAIVE = DATETIME_MAX_UTC.replace(tzinfo=None)
|
|
50
|
-
EPOCH_UTC = dt.datetime.fromtimestamp(0, tz=UTC)
|
|
51
|
-
EPOCH_DATE = EPOCH_UTC.date()
|
|
52
|
-
EPOCH_NAIVE = EPOCH_UTC.replace(tzinfo=None)
|
|
53
|
-
ZERO_TIME = dt.timedelta(0)
|
|
54
|
-
MICROSECOND = dt.timedelta(microseconds=1)
|
|
55
|
-
MILLISECOND = dt.timedelta(milliseconds=1)
|
|
56
|
-
SECOND = dt.timedelta(seconds=1)
|
|
57
|
-
MINUTE = dt.timedelta(minutes=1)
|
|
58
|
-
HOUR = dt.timedelta(hours=1)
|
|
59
|
-
DAY = dt.timedelta(days=1)
|
|
60
|
-
WEEK = dt.timedelta(weeks=1)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
##
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@overload
|
|
67
|
-
def add_duration(
|
|
68
|
-
date: dt.datetime, /, *, duration: Duration | None = ...
|
|
69
|
-
) -> dt.datetime: ...
|
|
70
|
-
@overload
|
|
71
|
-
def add_duration(date: dt.date, /, *, duration: Duration | None = ...) -> dt.date: ...
|
|
72
|
-
def add_duration(
|
|
73
|
-
date: DateOrDateTime, /, *, duration: Duration | None = None
|
|
74
|
-
) -> dt.date:
|
|
75
|
-
"""Add a duration to a date/datetime."""
|
|
76
|
-
if duration is None:
|
|
77
|
-
return date
|
|
78
|
-
if isinstance(date, dt.datetime):
|
|
79
|
-
return date + datetime_duration_to_timedelta(duration)
|
|
80
|
-
try:
|
|
81
|
-
timedelta = date_duration_to_timedelta(duration)
|
|
82
|
-
except DateDurationToTimeDeltaError:
|
|
83
|
-
raise AddDurationError(date=date, duration=duration) from None
|
|
84
|
-
return date + timedelta
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@dataclass(kw_only=True, slots=True)
|
|
88
|
-
class AddDurationError(Exception):
|
|
89
|
-
date: dt.date
|
|
90
|
-
duration: Duration
|
|
91
|
-
|
|
92
|
-
@override
|
|
93
|
-
def __str__(self) -> str:
|
|
94
|
-
return f"Date {self.date} must be paired with an integral duration; got {self.duration}"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def add_weekdays(date: dt.date, /, *, n: int = 1) -> dt.date:
|
|
101
|
-
"""Add a number of a weekdays to a given date.
|
|
102
|
-
|
|
103
|
-
If the initial date is a weekend, then moving to the adjacent weekday
|
|
104
|
-
counts as 1 move.
|
|
105
|
-
"""
|
|
106
|
-
check_date_not_datetime(date)
|
|
107
|
-
if n == 0 and not is_weekday(date):
|
|
108
|
-
raise AddWeekdaysError(date)
|
|
109
|
-
if n >= 1:
|
|
110
|
-
for _ in range(n):
|
|
111
|
-
date = round_to_next_weekday(date + DAY)
|
|
112
|
-
elif n <= -1:
|
|
113
|
-
for _ in range(-n):
|
|
114
|
-
date = round_to_prev_weekday(date - DAY)
|
|
115
|
-
return date
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class AddWeekdaysError(Exception): ...
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
##
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def are_equal_date_durations(x: Duration, y: Duration, /) -> bool:
|
|
125
|
-
"""Check if x == y for durations."""
|
|
126
|
-
x_timedelta = date_duration_to_timedelta(x)
|
|
127
|
-
y_timedelta = date_duration_to_timedelta(y)
|
|
128
|
-
return x_timedelta == y_timedelta
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def are_equal_dates_or_datetimes(
|
|
135
|
-
x: DateOrDateTime, y: DateOrDateTime, /, *, strict: bool = False
|
|
136
|
-
) -> bool:
|
|
137
|
-
"""Check if x == y for dates/datetimes."""
|
|
138
|
-
if is_instance_gen(x, dt.date) and is_instance_gen(y, dt.date):
|
|
139
|
-
return x == y
|
|
140
|
-
if is_instance_gen(x, dt.datetime) and is_instance_gen(y, dt.datetime):
|
|
141
|
-
return are_equal_datetimes(x, y, strict=strict)
|
|
142
|
-
raise AreEqualDatesOrDateTimesError(x=x, y=y)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@dataclass(kw_only=True, slots=True)
|
|
146
|
-
class AreEqualDatesOrDateTimesError(Exception):
|
|
147
|
-
x: DateOrDateTime
|
|
148
|
-
y: DateOrDateTime
|
|
149
|
-
|
|
150
|
-
@override
|
|
151
|
-
def __str__(self) -> str:
|
|
152
|
-
return f"Cannot compare date and datetime ({self.x}, {self.y})"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
##
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def are_equal_datetime_durations(x: Duration, y: Duration, /) -> bool:
|
|
159
|
-
"""Check if x == y for durations."""
|
|
160
|
-
x_timedelta = datetime_duration_to_timedelta(x)
|
|
161
|
-
y_timedelta = datetime_duration_to_timedelta(y)
|
|
162
|
-
return x_timedelta == y_timedelta
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
##
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def are_equal_datetimes(
|
|
169
|
-
x: dt.datetime, y: dt.datetime, /, *, strict: bool = False
|
|
170
|
-
) -> bool:
|
|
171
|
-
"""Check if x == y for datetimes."""
|
|
172
|
-
match x.tzinfo is None, y.tzinfo is None:
|
|
173
|
-
case True, True:
|
|
174
|
-
return x == y
|
|
175
|
-
case False, False if x == y:
|
|
176
|
-
return (x.tzinfo is y.tzinfo) or not strict
|
|
177
|
-
case False, False if x != y:
|
|
178
|
-
return False
|
|
179
|
-
case _:
|
|
180
|
-
raise AreEqualDateTimesError(x=x, y=y)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@dataclass(kw_only=True, slots=True)
|
|
184
|
-
class AreEqualDateTimesError(Exception):
|
|
185
|
-
x: dt.datetime
|
|
186
|
-
y: dt.datetime
|
|
187
|
-
|
|
188
|
-
@override
|
|
189
|
-
def __str__(self) -> str:
|
|
190
|
-
return f"Cannot compare local and zoned datetimes ({self.x}, {self.y})"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
##
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def are_equal_months(x: DateOrMonth, y: DateOrMonth, /) -> bool:
|
|
197
|
-
"""Check if x == y as months."""
|
|
198
|
-
x_month = Month.from_date(x) if isinstance(x, dt.date) else x
|
|
199
|
-
y_month = Month.from_date(y) if isinstance(y, dt.date) else y
|
|
200
|
-
return x_month == y_month
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def check_date_not_datetime(date: dt.date, /) -> None:
|
|
207
|
-
"""Check if a date is not a datetime."""
|
|
208
|
-
if not is_instance_gen(date, dt.date):
|
|
209
|
-
raise CheckDateNotDateTimeError(date=date)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@dataclass(kw_only=True, slots=True)
|
|
213
|
-
class CheckDateNotDateTimeError(Exception):
|
|
214
|
-
date: dt.date
|
|
215
|
-
|
|
216
|
-
@override
|
|
217
|
-
def __str__(self) -> str:
|
|
218
|
-
return f"Date must not be a datetime; got {self.date}"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
##
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def date_to_datetime(
|
|
225
|
-
date: dt.date, /, *, time: dt.time | None = None, time_zone: TimeZoneLike = UTC
|
|
226
|
-
) -> dt.datetime:
|
|
227
|
-
"""Expand a date into a datetime."""
|
|
228
|
-
check_date_not_datetime(date)
|
|
229
|
-
time_use = dt.time(0) if time is None else time
|
|
230
|
-
time_zone_use = ensure_time_zone(time_zone)
|
|
231
|
-
return dt.datetime.combine(date, time_use, tzinfo=time_zone_use)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
##
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def date_to_month(date: dt.date, /) -> Month:
|
|
238
|
-
"""Collapse a date into a month."""
|
|
239
|
-
check_date_not_datetime(date)
|
|
240
|
-
return Month(year=date.year, month=date.month)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
##
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def date_duration_to_int(duration: Duration, /) -> int:
|
|
247
|
-
"""Ensure a date duration is a float."""
|
|
248
|
-
match duration:
|
|
249
|
-
case int():
|
|
250
|
-
return duration
|
|
251
|
-
case float():
|
|
252
|
-
try:
|
|
253
|
-
return safe_round(duration)
|
|
254
|
-
except SafeRoundError:
|
|
255
|
-
raise _DateDurationToIntFloatError(duration=duration) from None
|
|
256
|
-
case dt.timedelta():
|
|
257
|
-
if is_integral_timedelta(duration):
|
|
258
|
-
return duration.days
|
|
259
|
-
raise _DateDurationToIntTimeDeltaError(duration=duration) from None
|
|
260
|
-
case _ as never:
|
|
261
|
-
assert_never(never)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
@dataclass(kw_only=True, slots=True)
|
|
265
|
-
class DateDurationToIntError(Exception):
|
|
266
|
-
duration: Duration
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
@dataclass(kw_only=True, slots=True)
|
|
270
|
-
class _DateDurationToIntFloatError(DateDurationToIntError):
|
|
271
|
-
@override
|
|
272
|
-
def __str__(self) -> str:
|
|
273
|
-
return f"Float duration must be integral; got {self.duration}"
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
@dataclass(kw_only=True, slots=True)
|
|
277
|
-
class _DateDurationToIntTimeDeltaError(DateDurationToIntError):
|
|
278
|
-
@override
|
|
279
|
-
def __str__(self) -> str:
|
|
280
|
-
return f"Timedelta duration must be integral; got {self.duration}"
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def date_duration_to_timedelta(duration: Duration, /) -> dt.timedelta:
|
|
284
|
-
"""Ensure a date duration is a timedelta."""
|
|
285
|
-
match duration:
|
|
286
|
-
case int():
|
|
287
|
-
return dt.timedelta(days=duration)
|
|
288
|
-
case float():
|
|
289
|
-
try:
|
|
290
|
-
as_int = safe_round(duration)
|
|
291
|
-
except SafeRoundError:
|
|
292
|
-
raise _DateDurationToTimeDeltaFloatError(duration=duration) from None
|
|
293
|
-
return dt.timedelta(days=as_int)
|
|
294
|
-
case dt.timedelta():
|
|
295
|
-
if is_integral_timedelta(duration):
|
|
296
|
-
return duration
|
|
297
|
-
raise _DateDurationToTimeDeltaTimeDeltaError(duration=duration) from None
|
|
298
|
-
case _ as never:
|
|
299
|
-
assert_never(never)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
@dataclass(kw_only=True, slots=True)
|
|
303
|
-
class DateDurationToTimeDeltaError(Exception):
|
|
304
|
-
duration: Duration
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
@dataclass(kw_only=True, slots=True)
|
|
308
|
-
class _DateDurationToTimeDeltaFloatError(DateDurationToTimeDeltaError):
|
|
309
|
-
@override
|
|
310
|
-
def __str__(self) -> str:
|
|
311
|
-
return f"Float duration must be integral; got {self.duration}"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
@dataclass(kw_only=True, slots=True)
|
|
315
|
-
class _DateDurationToTimeDeltaTimeDeltaError(DateDurationToTimeDeltaError):
|
|
316
|
-
@override
|
|
317
|
-
def __str__(self) -> str:
|
|
318
|
-
return f"Timedelta duration must be integral; got {self.duration}"
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
##
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def datetime_duration_to_float(duration: Duration, /) -> float:
|
|
325
|
-
"""Ensure a datetime duration is a float."""
|
|
326
|
-
match duration:
|
|
327
|
-
case int():
|
|
328
|
-
return float(duration)
|
|
329
|
-
case float():
|
|
330
|
-
return duration
|
|
331
|
-
case dt.timedelta():
|
|
332
|
-
return duration.total_seconds()
|
|
333
|
-
case _ as never:
|
|
334
|
-
assert_never(never)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def datetime_duration_to_microseconds(duration: Duration, /) -> int:
|
|
338
|
-
"""Compute the number of microseconds in a datetime duration."""
|
|
339
|
-
timedelta = datetime_duration_to_timedelta(duration)
|
|
340
|
-
return (
|
|
341
|
-
_MICROSECONDS_PER_DAY * timedelta.days
|
|
342
|
-
+ _MICROSECONDS_PER_SECOND * timedelta.seconds
|
|
343
|
-
+ timedelta.microseconds
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
@overload
|
|
348
|
-
def datetime_duration_to_milliseconds(
|
|
349
|
-
duration: Duration, /, *, strict: Literal[True]
|
|
350
|
-
) -> int: ...
|
|
351
|
-
@overload
|
|
352
|
-
def datetime_duration_to_milliseconds(
|
|
353
|
-
duration: Duration, /, *, strict: bool = False
|
|
354
|
-
) -> float: ...
|
|
355
|
-
def datetime_duration_to_milliseconds(
|
|
356
|
-
duration: Duration, /, *, strict: bool = False
|
|
357
|
-
) -> int | float:
|
|
358
|
-
"""Compute the number of milliseconds in a datetime duration."""
|
|
359
|
-
timedelta = datetime_duration_to_timedelta(duration)
|
|
360
|
-
microseconds = datetime_duration_to_microseconds(timedelta)
|
|
361
|
-
milliseconds, remainder = divmod(microseconds, _MICROSECONDS_PER_MILLISECOND)
|
|
362
|
-
match remainder, strict:
|
|
363
|
-
case 0, _:
|
|
364
|
-
return milliseconds
|
|
365
|
-
case _, True:
|
|
366
|
-
raise TimedeltaToMillisecondsError(duration=duration, remainder=remainder)
|
|
367
|
-
case _, False:
|
|
368
|
-
return milliseconds + remainder / _MICROSECONDS_PER_MILLISECOND
|
|
369
|
-
case _ as never:
|
|
370
|
-
assert_never(never)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
@dataclass(kw_only=True, slots=True)
|
|
374
|
-
class TimedeltaToMillisecondsError(Exception):
|
|
375
|
-
duration: Duration
|
|
376
|
-
remainder: int
|
|
377
|
-
|
|
378
|
-
@override
|
|
379
|
-
def __str__(self) -> str:
|
|
380
|
-
return f"Unable to convert {self.duration} to milliseconds; got {self.remainder} microsecond(s)"
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def datetime_duration_to_timedelta(duration: Duration, /) -> dt.timedelta:
|
|
384
|
-
"""Ensure a datetime duration is a timedelta."""
|
|
385
|
-
match duration:
|
|
386
|
-
case int() | float():
|
|
387
|
-
return dt.timedelta(seconds=duration)
|
|
388
|
-
case dt.timedelta():
|
|
389
|
-
return duration
|
|
390
|
-
case _ as never:
|
|
391
|
-
assert_never(never)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
##
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def datetime_utc(
|
|
398
|
-
year: int,
|
|
399
|
-
month: int,
|
|
400
|
-
day: int,
|
|
401
|
-
/,
|
|
402
|
-
hour: int = 0,
|
|
403
|
-
minute: int = 0,
|
|
404
|
-
second: int = 0,
|
|
405
|
-
microsecond: int = 0,
|
|
406
|
-
) -> dt.datetime:
|
|
407
|
-
"""Create a UTC-zoned datetime."""
|
|
408
|
-
return dt.datetime(
|
|
409
|
-
year,
|
|
410
|
-
month,
|
|
411
|
-
day,
|
|
412
|
-
hour=hour,
|
|
413
|
-
minute=minute,
|
|
414
|
-
second=second,
|
|
415
|
-
microsecond=microsecond,
|
|
416
|
-
tzinfo=UTC,
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
##
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def days_since_epoch(date: dt.date, /) -> int:
|
|
424
|
-
"""Compute the number of days since the epoch."""
|
|
425
|
-
check_date_not_datetime(date)
|
|
426
|
-
return timedelta_since_epoch(date).days
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def days_since_epoch_to_date(days: int, /) -> dt.date:
|
|
430
|
-
"""Convert a number of days since the epoch to a date."""
|
|
431
|
-
return EPOCH_DATE + days * DAY
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
##
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def ensure_month(month: MonthLike, /) -> Month:
|
|
438
|
-
"""Ensure the object is a month."""
|
|
439
|
-
if isinstance(month, Month):
|
|
440
|
-
return month
|
|
441
|
-
try:
|
|
442
|
-
return parse_month(month)
|
|
443
|
-
except ParseMonthError as error:
|
|
444
|
-
raise EnsureMonthError(month=error.month) from None
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
@dataclass(kw_only=True, slots=True)
|
|
448
|
-
class EnsureMonthError(Exception):
|
|
449
|
-
month: str
|
|
450
|
-
|
|
451
|
-
@override
|
|
452
|
-
def __str__(self) -> str:
|
|
453
|
-
return f"Unable to ensure month; got {self.month!r}"
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
##
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
def format_datetime_local_and_utc(datetime: dt.datetime, /) -> str:
|
|
460
|
-
"""Format a plain datetime locally & in UTC."""
|
|
461
|
-
time_zone = ensure_time_zone(datetime)
|
|
462
|
-
if time_zone is UTC:
|
|
463
|
-
return datetime.strftime("%Y-%m-%d %H:%M:%S (%a, UTC)")
|
|
464
|
-
as_utc = datetime.astimezone(UTC)
|
|
465
|
-
local = get_time_zone_name(time_zone)
|
|
466
|
-
if datetime.year != as_utc.year:
|
|
467
|
-
return f"{datetime:%Y-%m-%d %H:%M:%S (%a}, {local}, {as_utc:%Y-%m-%d %H:%M:%S} UTC)"
|
|
468
|
-
if (datetime.month != as_utc.month) or (datetime.day != as_utc.day):
|
|
469
|
-
return (
|
|
470
|
-
f"{datetime:%Y-%m-%d %H:%M:%S (%a}, {local}, {as_utc:%m-%d %H:%M:%S} UTC)"
|
|
471
|
-
)
|
|
472
|
-
return f"{datetime:%Y-%m-%d %H:%M:%S (%a}, {local}, {as_utc:%H:%M:%S} UTC)"
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
##
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
@overload
|
|
479
|
-
def get_date(*, date: MaybeCallableDate) -> dt.date: ...
|
|
480
|
-
@overload
|
|
481
|
-
def get_date(*, date: None) -> None: ...
|
|
482
|
-
@overload
|
|
483
|
-
def get_date(*, date: Sentinel) -> Sentinel: ...
|
|
484
|
-
@overload
|
|
485
|
-
def get_date(*, date: MaybeCallableDate | Sentinel) -> dt.date | Sentinel: ...
|
|
486
|
-
@overload
|
|
487
|
-
def get_date(
|
|
488
|
-
*, date: MaybeCallableDate | None | Sentinel = sentinel
|
|
489
|
-
) -> dt.date | None | Sentinel: ...
|
|
490
|
-
def get_date(
|
|
491
|
-
*, date: MaybeCallableDate | None | Sentinel = sentinel
|
|
492
|
-
) -> dt.date | None | Sentinel:
|
|
493
|
-
"""Get the date."""
|
|
494
|
-
match date:
|
|
495
|
-
case dt.date() | None | Sentinel():
|
|
496
|
-
return date
|
|
497
|
-
case Callable() as func:
|
|
498
|
-
return get_date(date=func())
|
|
499
|
-
case _ as never:
|
|
500
|
-
assert_never(never)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
##
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
@overload
|
|
507
|
-
def get_datetime(*, datetime: MaybeCallableDateTime) -> dt.datetime: ...
|
|
508
|
-
@overload
|
|
509
|
-
def get_datetime(*, datetime: None) -> None: ...
|
|
510
|
-
@overload
|
|
511
|
-
def get_datetime(*, datetime: Sentinel) -> Sentinel: ...
|
|
512
|
-
def get_datetime(
|
|
513
|
-
*, datetime: MaybeCallableDateTime | None | Sentinel = sentinel
|
|
514
|
-
) -> dt.datetime | None | Sentinel:
|
|
515
|
-
"""Get the datetime."""
|
|
516
|
-
match datetime:
|
|
517
|
-
case dt.datetime() | None | Sentinel():
|
|
518
|
-
return datetime
|
|
519
|
-
case Callable() as func:
|
|
520
|
-
return get_datetime(datetime=func())
|
|
521
|
-
case _ as never:
|
|
522
|
-
assert_never(never)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
##
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def get_half_years(*, n: int = 1) -> dt.timedelta:
|
|
529
|
-
"""Get a number of half-years as a timedelta."""
|
|
530
|
-
days_per_half_year = _DAYS_PER_YEAR / 2
|
|
531
|
-
return dt.timedelta(days=round(n * days_per_half_year))
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
HALF_YEAR = get_half_years(n=1)
|
|
535
|
-
|
|
536
|
-
##
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def get_min_max_date(
|
|
540
|
-
*,
|
|
541
|
-
min_date: dt.date | None = None,
|
|
542
|
-
max_date: dt.date | None = None,
|
|
543
|
-
min_age: Duration | None = None,
|
|
544
|
-
max_age: Duration | None = None,
|
|
545
|
-
time_zone: TimeZoneLike = UTC,
|
|
546
|
-
) -> tuple[dt.date | None, dt.date | None]:
|
|
547
|
-
"""Get the min/max date given a combination of dates/ages."""
|
|
548
|
-
today = get_today(time_zone=time_zone)
|
|
549
|
-
min_parts: Sequence[dt.date] = []
|
|
550
|
-
if min_date is not None:
|
|
551
|
-
if min_date > today:
|
|
552
|
-
raise _GetMinMaxDateMinDateError(min_date=min_date, today=today)
|
|
553
|
-
min_parts.append(min_date)
|
|
554
|
-
if max_age is not None:
|
|
555
|
-
if date_duration_to_timedelta(max_age) < ZERO_TIME:
|
|
556
|
-
raise _GetMinMaxDateMaxAgeError(max_age=max_age)
|
|
557
|
-
min_parts.append(sub_duration(today, duration=max_age))
|
|
558
|
-
min_date_use = max(min_parts, default=None)
|
|
559
|
-
max_parts: Sequence[dt.date] = []
|
|
560
|
-
if max_date is not None:
|
|
561
|
-
if max_date > today:
|
|
562
|
-
raise _GetMinMaxDateMaxDateError(max_date=max_date, today=today)
|
|
563
|
-
max_parts.append(max_date)
|
|
564
|
-
if min_age is not None:
|
|
565
|
-
if date_duration_to_timedelta(min_age) < ZERO_TIME:
|
|
566
|
-
raise _GetMinMaxDateMinAgeError(min_age=min_age)
|
|
567
|
-
max_parts.append(sub_duration(today, duration=min_age))
|
|
568
|
-
max_date_use = min(max_parts, default=None)
|
|
569
|
-
if (
|
|
570
|
-
(min_date_use is not None)
|
|
571
|
-
and (max_date_use is not None)
|
|
572
|
-
and (min_date_use > max_date_use)
|
|
573
|
-
):
|
|
574
|
-
raise _GetMinMaxDatePeriodError(min_date=min_date_use, max_date=max_date_use)
|
|
575
|
-
return min_date_use, max_date_use
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
@dataclass(kw_only=True, slots=True)
|
|
579
|
-
class GetMinMaxDateError(Exception): ...
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
@dataclass(kw_only=True, slots=True)
|
|
583
|
-
class _GetMinMaxDateMinDateError(GetMinMaxDateError):
|
|
584
|
-
min_date: dt.date
|
|
585
|
-
today: dt.date
|
|
586
|
-
|
|
587
|
-
@override
|
|
588
|
-
def __str__(self) -> str:
|
|
589
|
-
return f"Min date must be at most today; got {self.min_date} > {self.today}"
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
@dataclass(kw_only=True, slots=True)
|
|
593
|
-
class _GetMinMaxDateMinAgeError(GetMinMaxDateError):
|
|
594
|
-
min_age: Duration
|
|
595
|
-
|
|
596
|
-
@override
|
|
597
|
-
def __str__(self) -> str:
|
|
598
|
-
return f"Min age must be non-negative; got {self.min_age}"
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
@dataclass(kw_only=True, slots=True)
|
|
602
|
-
class _GetMinMaxDateMaxDateError(GetMinMaxDateError):
|
|
603
|
-
max_date: dt.date
|
|
604
|
-
today: dt.date
|
|
605
|
-
|
|
606
|
-
@override
|
|
607
|
-
def __str__(self) -> str:
|
|
608
|
-
return f"Max date must be at most today; got {self.max_date} > {self.today}"
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
@dataclass(kw_only=True, slots=True)
|
|
612
|
-
class _GetMinMaxDateMaxAgeError(GetMinMaxDateError):
|
|
613
|
-
max_age: Duration
|
|
614
|
-
|
|
615
|
-
@override
|
|
616
|
-
def __str__(self) -> str:
|
|
617
|
-
return f"Max age must be non-negative; got {self.max_age}"
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
@dataclass(kw_only=True, slots=True)
|
|
621
|
-
class _GetMinMaxDatePeriodError(GetMinMaxDateError):
|
|
622
|
-
min_date: dt.date
|
|
623
|
-
max_date: dt.date
|
|
624
|
-
|
|
625
|
-
@override
|
|
626
|
-
def __str__(self) -> str:
|
|
627
|
-
return (
|
|
628
|
-
f"Min date must be at most max date; got {self.min_date} > {self.max_date}"
|
|
629
|
-
)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
##
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
def get_months(*, n: int = 1) -> dt.timedelta:
|
|
636
|
-
"""Get a number of months as a timedelta."""
|
|
637
|
-
days_per_month = _DAYS_PER_YEAR / 12
|
|
638
|
-
return dt.timedelta(days=round(n * days_per_month))
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
MONTH = get_months(n=1)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
##
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
def get_now(*, time_zone: TimeZoneLike = UTC) -> dt.datetime:
|
|
648
|
-
"""Get the current, timezone-aware time."""
|
|
649
|
-
return dt.datetime.now(tz=ensure_time_zone(time_zone))
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
NOW_UTC = get_now(time_zone=UTC)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
##
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
def get_quarters(*, n: int = 1) -> dt.timedelta:
|
|
659
|
-
"""Get a number of quarters as a timedelta."""
|
|
660
|
-
days_per_quarter = _DAYS_PER_YEAR / 4
|
|
661
|
-
return dt.timedelta(days=round(n * days_per_quarter))
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
QUARTER = get_quarters(n=1)
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
##
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
def get_today(*, time_zone: TimeZoneLike = UTC) -> dt.date:
|
|
671
|
-
"""Get the current, timezone-aware date."""
|
|
672
|
-
return get_now(time_zone=time_zone).date()
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
TODAY_UTC = get_today(time_zone=UTC)
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
##
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def get_years(*, n: int = 1) -> dt.timedelta:
|
|
682
|
-
"""Get a number of years as a timedelta."""
|
|
683
|
-
return dt.timedelta(days=round(n * _DAYS_PER_YEAR))
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
YEAR = get_years(n=1)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
##
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
def is_integral_timedelta(duration: Duration, /) -> bool:
|
|
693
|
-
"""Check if a duration is integral."""
|
|
694
|
-
timedelta = datetime_duration_to_timedelta(duration)
|
|
695
|
-
return (timedelta.seconds == 0) and (timedelta.microseconds == 0)
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
##
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
def is_plain_datetime(obj: Any, /) -> TypeGuard[dt.datetime]:
|
|
702
|
-
"""Check if an object is a plain datetime."""
|
|
703
|
-
return isinstance(obj, dt.datetime) and (obj.tzinfo is None)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
##
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
_FRIDAY = 5
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
def is_weekday(date: dt.date, /) -> bool:
|
|
713
|
-
"""Check if a date is a weekday."""
|
|
714
|
-
check_date_not_datetime(date)
|
|
715
|
-
return date.isoweekday() <= _FRIDAY
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
##
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
def is_zero_time(duration: Duration, /) -> bool:
|
|
722
|
-
"""Check if a timedelta is 0."""
|
|
723
|
-
return datetime_duration_to_timedelta(duration) == ZERO_TIME
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
##
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
def is_zoned_datetime(obj: Any, /) -> TypeGuard[dt.datetime]:
|
|
730
|
-
"""Check if an object is a zoned datetime."""
|
|
731
|
-
return isinstance(obj, dt.datetime) and (obj.tzinfo is not None)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
##
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def maybe_sub_pct_y(text: str, /) -> str:
|
|
738
|
-
"""Substitute the `%Y' token with '%4Y' if necessary."""
|
|
739
|
-
match SYSTEM:
|
|
740
|
-
case "windows": # skipif-not-windows
|
|
741
|
-
return text
|
|
742
|
-
case "mac": # skipif-not-macos
|
|
743
|
-
return text
|
|
744
|
-
case "linux": # skipif-not-linux
|
|
745
|
-
return sub("%Y", "%4Y", text)
|
|
746
|
-
case _ as never:
|
|
747
|
-
assert_never(never)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
##
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
def mean_datetime(
|
|
754
|
-
datetimes: Iterable[dt.datetime],
|
|
755
|
-
/,
|
|
756
|
-
*,
|
|
757
|
-
weights: Iterable[SupportsFloat] | None = None,
|
|
758
|
-
mode: RoundMode = "standard",
|
|
759
|
-
rel_tol: float | None = None,
|
|
760
|
-
abs_tol: float | None = None,
|
|
761
|
-
) -> dt.datetime:
|
|
762
|
-
"""Compute the mean of a set of datetimes."""
|
|
763
|
-
datetimes = list(datetimes)
|
|
764
|
-
match len(datetimes):
|
|
765
|
-
case 0:
|
|
766
|
-
raise MeanDateTimeError from None
|
|
767
|
-
case 1:
|
|
768
|
-
return one(datetimes)
|
|
769
|
-
case _:
|
|
770
|
-
microseconds = list(map(microseconds_since_epoch, datetimes))
|
|
771
|
-
mean_float = fmean(microseconds, weights=weights)
|
|
772
|
-
mean_int = round_(mean_float, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol)
|
|
773
|
-
return microseconds_since_epoch_to_datetime(
|
|
774
|
-
mean_int, time_zone=datetimes[0].tzinfo
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
@dataclass(kw_only=True, slots=True)
|
|
779
|
-
class MeanDateTimeError(Exception):
|
|
780
|
-
@override
|
|
781
|
-
def __str__(self) -> str:
|
|
782
|
-
return "Mean requires at least 1 datetime"
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
##
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
def mean_timedelta(
|
|
789
|
-
timedeltas: Iterable[dt.timedelta],
|
|
790
|
-
/,
|
|
791
|
-
*,
|
|
792
|
-
weights: Iterable[SupportsFloat] | None = None,
|
|
793
|
-
mode: RoundMode = "standard",
|
|
794
|
-
rel_tol: float | None = None,
|
|
795
|
-
abs_tol: float | None = None,
|
|
796
|
-
) -> dt.timedelta:
|
|
797
|
-
"""Compute the mean of a set of timedeltas."""
|
|
798
|
-
timedeltas = list(timedeltas)
|
|
799
|
-
match len(timedeltas):
|
|
800
|
-
case 0:
|
|
801
|
-
raise MeanTimeDeltaError from None
|
|
802
|
-
case 1:
|
|
803
|
-
return one(timedeltas)
|
|
804
|
-
case _:
|
|
805
|
-
microseconds = list(map(datetime_duration_to_microseconds, timedeltas))
|
|
806
|
-
mean_float = fmean(microseconds, weights=weights)
|
|
807
|
-
mean_int = round_(mean_float, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol)
|
|
808
|
-
return microseconds_to_timedelta(mean_int)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
@dataclass(kw_only=True, slots=True)
|
|
812
|
-
class MeanTimeDeltaError(Exception):
|
|
813
|
-
@override
|
|
814
|
-
def __str__(self) -> str:
|
|
815
|
-
return "Mean requires at least 1 timedelta"
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
##
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
def microseconds_since_epoch(datetime: dt.datetime, /) -> int:
|
|
822
|
-
"""Compute the number of microseconds since the epoch."""
|
|
823
|
-
return datetime_duration_to_microseconds(timedelta_since_epoch(datetime))
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
def microseconds_to_timedelta(microseconds: int, /) -> dt.timedelta:
|
|
827
|
-
"""Compute a timedelta given a number of microseconds."""
|
|
828
|
-
if microseconds == 0:
|
|
829
|
-
return ZERO_TIME
|
|
830
|
-
if microseconds >= 1:
|
|
831
|
-
days, remainder = divmod(microseconds, _MICROSECONDS_PER_DAY)
|
|
832
|
-
seconds, micros = divmod(remainder, _MICROSECONDS_PER_SECOND)
|
|
833
|
-
return dt.timedelta(days=days, seconds=seconds, microseconds=micros)
|
|
834
|
-
return -microseconds_to_timedelta(-microseconds)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
def microseconds_since_epoch_to_datetime(
|
|
838
|
-
microseconds: int, /, *, time_zone: dt.tzinfo | None = None
|
|
839
|
-
) -> dt.datetime:
|
|
840
|
-
"""Convert a number of microseconds since the epoch to a datetime."""
|
|
841
|
-
epoch = EPOCH_NAIVE if time_zone is None else EPOCH_UTC
|
|
842
|
-
timedelta = microseconds_to_timedelta(microseconds)
|
|
843
|
-
return epoch + timedelta
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
##
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
@overload
|
|
850
|
-
def milliseconds_since_epoch(
|
|
851
|
-
datetime: dt.datetime, /, *, strict: Literal[True]
|
|
852
|
-
) -> int: ...
|
|
853
|
-
@overload
|
|
854
|
-
def milliseconds_since_epoch(
|
|
855
|
-
datetime: dt.datetime, /, *, strict: bool = False
|
|
856
|
-
) -> float: ...
|
|
857
|
-
def milliseconds_since_epoch(
|
|
858
|
-
datetime: dt.datetime, /, *, strict: bool = False
|
|
859
|
-
) -> float:
|
|
860
|
-
"""Compute the number of milliseconds since the epoch."""
|
|
861
|
-
microseconds = microseconds_since_epoch(datetime)
|
|
862
|
-
milliseconds, remainder = divmod(microseconds, _MICROSECONDS_PER_MILLISECOND)
|
|
863
|
-
if strict:
|
|
864
|
-
if remainder == 0:
|
|
865
|
-
return milliseconds
|
|
866
|
-
raise MillisecondsSinceEpochError(datetime=datetime, remainder=remainder)
|
|
867
|
-
return milliseconds + remainder / _MICROSECONDS_PER_MILLISECOND
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
@dataclass(kw_only=True, slots=True)
|
|
871
|
-
class MillisecondsSinceEpochError(Exception):
|
|
872
|
-
datetime: dt.datetime
|
|
873
|
-
remainder: int
|
|
874
|
-
|
|
875
|
-
@override
|
|
876
|
-
def __str__(self) -> str:
|
|
877
|
-
return f"Unable to convert {self.datetime} to milliseconds since epoch; got {self.remainder} microsecond(s)"
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
def milliseconds_since_epoch_to_datetime(
|
|
881
|
-
milliseconds: int, /, *, time_zone: dt.tzinfo | None = None
|
|
882
|
-
) -> dt.datetime:
|
|
883
|
-
"""Convert a number of milliseconds since the epoch to a datetime."""
|
|
884
|
-
epoch = EPOCH_NAIVE if time_zone is None else EPOCH_UTC
|
|
885
|
-
timedelta = milliseconds_to_timedelta(milliseconds)
|
|
886
|
-
return epoch + timedelta
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
def milliseconds_to_timedelta(milliseconds: int, /) -> dt.timedelta:
|
|
890
|
-
"""Compute a timedelta given a number of milliseconds."""
|
|
891
|
-
return microseconds_to_timedelta(_MICROSECONDS_PER_MILLISECOND * milliseconds)
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
##
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
@dataclass(order=True, unsafe_hash=True, slots=True)
|
|
898
|
-
class Month:
|
|
899
|
-
"""Represents a month in time."""
|
|
900
|
-
|
|
901
|
-
year: int
|
|
902
|
-
month: int
|
|
903
|
-
|
|
904
|
-
def __post_init__(self) -> None:
|
|
905
|
-
try:
|
|
906
|
-
_ = dt.date(self.year, self.month, 1)
|
|
907
|
-
except ValueError:
|
|
908
|
-
raise MonthError(year=self.year, month=self.month) from None
|
|
909
|
-
|
|
910
|
-
@override
|
|
911
|
-
def __repr__(self) -> str:
|
|
912
|
-
return serialize_month(self)
|
|
913
|
-
|
|
914
|
-
@override
|
|
915
|
-
def __str__(self) -> str:
|
|
916
|
-
return repr(self)
|
|
917
|
-
|
|
918
|
-
def __add__(self, other: Any, /) -> Self:
|
|
919
|
-
if not isinstance(other, int): # pragma: no cover
|
|
920
|
-
return NotImplemented
|
|
921
|
-
years, month = divmod(self.month + other - 1, 12)
|
|
922
|
-
month += 1
|
|
923
|
-
year = self.year + years
|
|
924
|
-
return replace(self, year=year, month=month)
|
|
925
|
-
|
|
926
|
-
@overload
|
|
927
|
-
def __sub__(self, other: Self, /) -> int: ...
|
|
928
|
-
@overload
|
|
929
|
-
def __sub__(self, other: int, /) -> Self: ...
|
|
930
|
-
def __sub__(self, other: Self | int, /) -> Self | int:
|
|
931
|
-
if isinstance(other, int): # pragma: no cover
|
|
932
|
-
return self + (-other)
|
|
933
|
-
if isinstance(other, type(self)):
|
|
934
|
-
self_as_int = 12 * self.year + self.month
|
|
935
|
-
other_as_int = 12 * other.year + other.month
|
|
936
|
-
return self_as_int - other_as_int
|
|
937
|
-
return NotImplemented # pragma: no cover
|
|
938
|
-
|
|
939
|
-
@classmethod
|
|
940
|
-
def from_date(cls, date: dt.date, /) -> Self:
|
|
941
|
-
check_date_not_datetime(date)
|
|
942
|
-
return cls(year=date.year, month=date.month)
|
|
943
|
-
|
|
944
|
-
def to_date(self, /, *, day: int = 1) -> dt.date:
|
|
945
|
-
return dt.date(self.year, self.month, day)
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
@dataclass(kw_only=True, slots=True)
|
|
949
|
-
class MonthError(Exception):
|
|
950
|
-
year: int
|
|
951
|
-
month: int
|
|
952
|
-
|
|
953
|
-
@override
|
|
954
|
-
def __str__(self) -> str:
|
|
955
|
-
return f"Invalid year and month: {self.year}, {self.month}"
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
type DateOrMonth = dt.date | Month
|
|
959
|
-
type MonthLike = MaybeStr[Month]
|
|
960
|
-
MIN_MONTH = Month(dt.date.min.year, dt.date.min.month)
|
|
961
|
-
MAX_MONTH = Month(dt.date.max.year, dt.date.max.month)
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
##
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
_TWO_DIGIT_YEAR_MIN = 1969
|
|
968
|
-
_TWO_DIGIT_YEAR_MAX = _TWO_DIGIT_YEAR_MIN + 99
|
|
969
|
-
MIN_DATE_TWO_DIGIT_YEAR = dt.date(
|
|
970
|
-
_TWO_DIGIT_YEAR_MIN, dt.date.min.month, dt.date.min.day
|
|
971
|
-
)
|
|
972
|
-
MAX_DATE_TWO_DIGIT_YEAR = dt.date(
|
|
973
|
-
_TWO_DIGIT_YEAR_MAX, dt.date.max.month, dt.date.max.day
|
|
974
|
-
)
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
def parse_two_digit_year(year: int | str, /) -> int:
|
|
978
|
-
"""Parse a 2-digit year into a year."""
|
|
979
|
-
match year:
|
|
980
|
-
case int():
|
|
981
|
-
years = range(_TWO_DIGIT_YEAR_MIN, _TWO_DIGIT_YEAR_MAX + 1)
|
|
982
|
-
try:
|
|
983
|
-
return one(y for y in years if y % 100 == year)
|
|
984
|
-
except OneEmptyError:
|
|
985
|
-
raise _ParseTwoDigitYearInvalidIntegerError(year=year) from None
|
|
986
|
-
case str():
|
|
987
|
-
if search(r"^\d{1,2}$", year):
|
|
988
|
-
return parse_two_digit_year(int(year))
|
|
989
|
-
raise _ParseTwoDigitYearInvalidStringError(year=year)
|
|
990
|
-
case _ as never:
|
|
991
|
-
assert_never(never)
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
@dataclass(kw_only=True, slots=True)
|
|
995
|
-
class ParseTwoDigitYearError(Exception):
|
|
996
|
-
year: int | str
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
@dataclass(kw_only=True, slots=True)
|
|
1000
|
-
class _ParseTwoDigitYearInvalidIntegerError(Exception):
|
|
1001
|
-
year: int | str
|
|
1002
|
-
|
|
1003
|
-
@override
|
|
1004
|
-
def __str__(self) -> str:
|
|
1005
|
-
return f"Unable to parse year; got {self.year!r}"
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
@dataclass(kw_only=True, slots=True)
|
|
1009
|
-
class _ParseTwoDigitYearInvalidStringError(Exception):
|
|
1010
|
-
year: int | str
|
|
1011
|
-
|
|
1012
|
-
@override
|
|
1013
|
-
def __str__(self) -> str:
|
|
1014
|
-
return f"Unable to parse year; got {self.year!r}"
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
##
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
def round_datetime(
|
|
1021
|
-
datetime: dt.datetime,
|
|
1022
|
-
duration: Duration,
|
|
1023
|
-
/,
|
|
1024
|
-
*,
|
|
1025
|
-
mode: RoundMode = "standard",
|
|
1026
|
-
rel_tol: float | None = None,
|
|
1027
|
-
abs_tol: float | None = None,
|
|
1028
|
-
) -> dt.datetime:
|
|
1029
|
-
"""Round a datetime to a timedelta."""
|
|
1030
|
-
if datetime.tzinfo is None:
|
|
1031
|
-
dividend = microseconds_since_epoch(datetime)
|
|
1032
|
-
divisor = datetime_duration_to_microseconds(duration)
|
|
1033
|
-
quotient, remainder = divmod(dividend, divisor)
|
|
1034
|
-
rnd_remainder = round_(
|
|
1035
|
-
remainder / divisor, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
|
|
1036
|
-
)
|
|
1037
|
-
rnd_quotient = quotient + rnd_remainder
|
|
1038
|
-
microseconds = rnd_quotient * divisor
|
|
1039
|
-
return microseconds_since_epoch_to_datetime(microseconds)
|
|
1040
|
-
local = datetime.replace(tzinfo=None)
|
|
1041
|
-
rounded = round_datetime(
|
|
1042
|
-
local, duration, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
|
|
1043
|
-
)
|
|
1044
|
-
return rounded.replace(tzinfo=datetime.tzinfo)
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
##
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
def round_to_next_weekday(date: dt.date, /) -> dt.date:
|
|
1051
|
-
"""Round a date to the next weekday."""
|
|
1052
|
-
return _round_to_weekday(date, prev_or_next="next")
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
def round_to_prev_weekday(date: dt.date, /) -> dt.date:
|
|
1056
|
-
"""Round a date to the previous weekday."""
|
|
1057
|
-
return _round_to_weekday(date, prev_or_next="prev")
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
def _round_to_weekday(
|
|
1061
|
-
date: dt.date, /, *, prev_or_next: Literal["prev", "next"]
|
|
1062
|
-
) -> dt.date:
|
|
1063
|
-
"""Round a date to the previous weekday."""
|
|
1064
|
-
check_date_not_datetime(date)
|
|
1065
|
-
match prev_or_next:
|
|
1066
|
-
case "prev":
|
|
1067
|
-
n = -1
|
|
1068
|
-
case "next":
|
|
1069
|
-
n = 1
|
|
1070
|
-
case _ as never:
|
|
1071
|
-
assert_never(never)
|
|
1072
|
-
while not is_weekday(date):
|
|
1073
|
-
date = add_weekdays(date, n=n)
|
|
1074
|
-
return date
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
##
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
def serialize_compact(date_or_datetime: DateOrDateTime, /) -> str:
|
|
1081
|
-
"""Serialize a date/datetime using a compact format."""
|
|
1082
|
-
match date_or_datetime:
|
|
1083
|
-
case dt.datetime() as datetime:
|
|
1084
|
-
if datetime.tzinfo is None:
|
|
1085
|
-
raise SerializeCompactError(datetime=datetime)
|
|
1086
|
-
format_ = "%Y%m%dT%H%M%S"
|
|
1087
|
-
case dt.date():
|
|
1088
|
-
format_ = "%Y%m%d"
|
|
1089
|
-
case _ as never:
|
|
1090
|
-
assert_never(never)
|
|
1091
|
-
return date_or_datetime.strftime(maybe_sub_pct_y(format_))
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
@dataclass(kw_only=True, slots=True)
|
|
1095
|
-
class SerializeCompactError(Exception):
|
|
1096
|
-
datetime: dt.datetime
|
|
1097
|
-
|
|
1098
|
-
@override
|
|
1099
|
-
def __str__(self) -> str:
|
|
1100
|
-
return f"Unable to serialize plain datetime {self.datetime}"
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
def parse_date_compact(text: str, /) -> dt.date:
|
|
1104
|
-
"""Parse a compact string into a date."""
|
|
1105
|
-
try:
|
|
1106
|
-
datetime = dt.datetime.strptime(text, "%Y%m%d").replace(tzinfo=UTC)
|
|
1107
|
-
except ValueError:
|
|
1108
|
-
raise ParseDateCompactError(text=text) from None
|
|
1109
|
-
return datetime.date()
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
@dataclass(kw_only=True, slots=True)
|
|
1113
|
-
class ParseDateCompactError(Exception):
|
|
1114
|
-
text: str
|
|
1115
|
-
|
|
1116
|
-
@override
|
|
1117
|
-
def __str__(self) -> str:
|
|
1118
|
-
return f"Unable to parse {self.text!r} into a date"
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
def parse_datetime_compact(
|
|
1122
|
-
text: str, /, *, time_zone: TimeZoneLike = UTC
|
|
1123
|
-
) -> dt.datetime:
|
|
1124
|
-
"""Parse a compact string into a datetime."""
|
|
1125
|
-
time_zone = ensure_time_zone(time_zone)
|
|
1126
|
-
try:
|
|
1127
|
-
return dt.datetime.strptime(text, "%Y%m%dT%H%M%S").replace(tzinfo=time_zone)
|
|
1128
|
-
except ValueError:
|
|
1129
|
-
raise ParseDateTimeCompactError(text=text) from None
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
@dataclass(kw_only=True, slots=True)
|
|
1133
|
-
class ParseDateTimeCompactError(Exception):
|
|
1134
|
-
text: str
|
|
1135
|
-
|
|
1136
|
-
@override
|
|
1137
|
-
def __str__(self) -> str:
|
|
1138
|
-
return f"Unable to parse {self.text!r} into a datetime"
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
##
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
def serialize_month(month: Month, /) -> str:
|
|
1145
|
-
"""Serialize a month."""
|
|
1146
|
-
return f"{month.year:04}-{month.month:02}"
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
def parse_month(month: str, /) -> Month:
|
|
1150
|
-
"""Parse a string into a month."""
|
|
1151
|
-
for fmt in ["%Y-%m", "%Y%m", "%Y %m"]:
|
|
1152
|
-
try:
|
|
1153
|
-
date = dt.datetime.strptime(month, fmt).replace(tzinfo=UTC).date()
|
|
1154
|
-
except ValueError:
|
|
1155
|
-
pass
|
|
1156
|
-
else:
|
|
1157
|
-
return Month(date.year, date.month)
|
|
1158
|
-
raise ParseMonthError(month=month)
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
@dataclass(kw_only=True, slots=True)
|
|
1162
|
-
class ParseMonthError(Exception):
|
|
1163
|
-
month: str
|
|
1164
|
-
|
|
1165
|
-
@override
|
|
1166
|
-
def __str__(self) -> str:
|
|
1167
|
-
return f"Unable to parse month; got {self.month!r}"
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
##
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
@overload
|
|
1174
|
-
def sub_duration(
|
|
1175
|
-
date: dt.datetime, /, *, duration: Duration | None = ...
|
|
1176
|
-
) -> dt.datetime: ...
|
|
1177
|
-
@overload
|
|
1178
|
-
def sub_duration(date: dt.date, /, *, duration: Duration | None = ...) -> dt.date: ...
|
|
1179
|
-
def sub_duration(
|
|
1180
|
-
date: DateOrDateTime, /, *, duration: Duration | None = None
|
|
1181
|
-
) -> dt.date:
|
|
1182
|
-
"""Subtract a duration from a date/datetime."""
|
|
1183
|
-
if duration is None:
|
|
1184
|
-
return date
|
|
1185
|
-
try:
|
|
1186
|
-
return add_duration(date, duration=-duration)
|
|
1187
|
-
except AddDurationError:
|
|
1188
|
-
raise SubDurationError(date=date, duration=duration) from None
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
@dataclass(kw_only=True, slots=True)
|
|
1192
|
-
class SubDurationError(Exception):
|
|
1193
|
-
date: dt.date
|
|
1194
|
-
duration: Duration
|
|
1195
|
-
|
|
1196
|
-
@override
|
|
1197
|
-
def __str__(self) -> str:
|
|
1198
|
-
return f"Date {self.date} must be paired with an integral duration; got {self.duration}"
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
##
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
def timedelta_since_epoch(date_or_datetime: DateOrDateTime, /) -> dt.timedelta:
|
|
1205
|
-
"""Compute the timedelta since the epoch."""
|
|
1206
|
-
match date_or_datetime:
|
|
1207
|
-
case dt.datetime() as datetime:
|
|
1208
|
-
if datetime.tzinfo is None:
|
|
1209
|
-
return datetime - EPOCH_NAIVE
|
|
1210
|
-
return datetime.astimezone(UTC) - EPOCH_UTC
|
|
1211
|
-
case dt.date() as date:
|
|
1212
|
-
return date - EPOCH_DATE
|
|
1213
|
-
case _ as never:
|
|
1214
|
-
assert_never(never)
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
##
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
def yield_days(
|
|
1221
|
-
*, start: dt.date | None = None, end: dt.date | None = None, days: int | None = None
|
|
1222
|
-
) -> Iterator[dt.date]:
|
|
1223
|
-
"""Yield the days in a range."""
|
|
1224
|
-
match start, end, days:
|
|
1225
|
-
case dt.date(), dt.date(), None:
|
|
1226
|
-
check_date_not_datetime(start)
|
|
1227
|
-
check_date_not_datetime(end)
|
|
1228
|
-
date = start
|
|
1229
|
-
while date <= end:
|
|
1230
|
-
yield date
|
|
1231
|
-
date += DAY
|
|
1232
|
-
case dt.date(), None, int():
|
|
1233
|
-
check_date_not_datetime(start)
|
|
1234
|
-
date = start
|
|
1235
|
-
for _ in range(days):
|
|
1236
|
-
yield date
|
|
1237
|
-
date += DAY
|
|
1238
|
-
case None, dt.date(), int():
|
|
1239
|
-
check_date_not_datetime(end)
|
|
1240
|
-
date = end
|
|
1241
|
-
for _ in range(days):
|
|
1242
|
-
yield date
|
|
1243
|
-
date -= DAY
|
|
1244
|
-
case _:
|
|
1245
|
-
raise YieldDaysError(start=start, end=end, days=days)
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
@dataclass(kw_only=True, slots=True)
|
|
1249
|
-
class YieldDaysError(Exception):
|
|
1250
|
-
start: dt.date | None
|
|
1251
|
-
end: dt.date | None
|
|
1252
|
-
days: int | None
|
|
1253
|
-
|
|
1254
|
-
@override
|
|
1255
|
-
def __str__(self) -> str:
|
|
1256
|
-
return (
|
|
1257
|
-
f"Invalid arguments: start={self.start}, end={self.end}, days={self.days}"
|
|
1258
|
-
)
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
##
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
def yield_weekdays(
|
|
1265
|
-
*, start: dt.date | None = None, end: dt.date | None = None, days: int | None = None
|
|
1266
|
-
) -> Iterator[dt.date]:
|
|
1267
|
-
"""Yield the weekdays in a range."""
|
|
1268
|
-
match start, end, days:
|
|
1269
|
-
case dt.date(), dt.date(), None:
|
|
1270
|
-
check_date_not_datetime(start)
|
|
1271
|
-
check_date_not_datetime(end)
|
|
1272
|
-
date = round_to_next_weekday(start)
|
|
1273
|
-
while date <= end:
|
|
1274
|
-
yield date
|
|
1275
|
-
date = round_to_next_weekday(date + DAY)
|
|
1276
|
-
case dt.date(), None, int():
|
|
1277
|
-
check_date_not_datetime(start)
|
|
1278
|
-
date = round_to_next_weekday(start)
|
|
1279
|
-
for _ in range(days):
|
|
1280
|
-
yield date
|
|
1281
|
-
date = round_to_next_weekday(date + DAY)
|
|
1282
|
-
case None, dt.date(), int():
|
|
1283
|
-
check_date_not_datetime(end)
|
|
1284
|
-
date = round_to_prev_weekday(end)
|
|
1285
|
-
for _ in range(days):
|
|
1286
|
-
yield date
|
|
1287
|
-
date = round_to_prev_weekday(date - DAY)
|
|
1288
|
-
case _:
|
|
1289
|
-
raise YieldWeekdaysError(start=start, end=end, days=days)
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
@dataclass(kw_only=True, slots=True)
|
|
1293
|
-
class YieldWeekdaysError(Exception):
|
|
1294
|
-
start: dt.date | None
|
|
1295
|
-
end: dt.date | None
|
|
1296
|
-
days: int | None
|
|
1297
|
-
|
|
1298
|
-
@override
|
|
1299
|
-
def __str__(self) -> str:
|
|
1300
|
-
return (
|
|
1301
|
-
f"Invalid arguments: start={self.start}, end={self.end}, days={self.days}"
|
|
1302
|
-
)
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
__all__ = [
|
|
1306
|
-
"DATETIME_MAX_NAIVE",
|
|
1307
|
-
"DATETIME_MAX_UTC",
|
|
1308
|
-
"DATETIME_MIN_NAIVE",
|
|
1309
|
-
"DATETIME_MIN_UTC",
|
|
1310
|
-
"DAY",
|
|
1311
|
-
"EPOCH_DATE",
|
|
1312
|
-
"EPOCH_NAIVE",
|
|
1313
|
-
"EPOCH_UTC",
|
|
1314
|
-
"HALF_YEAR",
|
|
1315
|
-
"HOUR",
|
|
1316
|
-
"MAX_DATE_TWO_DIGIT_YEAR",
|
|
1317
|
-
"MAX_MONTH",
|
|
1318
|
-
"MILLISECOND",
|
|
1319
|
-
"MINUTE",
|
|
1320
|
-
"MIN_DATE_TWO_DIGIT_YEAR",
|
|
1321
|
-
"MIN_MONTH",
|
|
1322
|
-
"MONTH",
|
|
1323
|
-
"NOW_UTC",
|
|
1324
|
-
"QUARTER",
|
|
1325
|
-
"SECOND",
|
|
1326
|
-
"TODAY_UTC",
|
|
1327
|
-
"WEEK",
|
|
1328
|
-
"YEAR",
|
|
1329
|
-
"ZERO_TIME",
|
|
1330
|
-
"AddDurationError",
|
|
1331
|
-
"AddWeekdaysError",
|
|
1332
|
-
"AreEqualDateTimesError",
|
|
1333
|
-
"AreEqualDatesOrDateTimesError",
|
|
1334
|
-
"CheckDateNotDateTimeError",
|
|
1335
|
-
"DateOrMonth",
|
|
1336
|
-
"EnsureMonthError",
|
|
1337
|
-
"GetMinMaxDateError",
|
|
1338
|
-
"MeanDateTimeError",
|
|
1339
|
-
"MeanTimeDeltaError",
|
|
1340
|
-
"MillisecondsSinceEpochError",
|
|
1341
|
-
"Month",
|
|
1342
|
-
"MonthError",
|
|
1343
|
-
"MonthLike",
|
|
1344
|
-
"ParseDateCompactError",
|
|
1345
|
-
"ParseDateTimeCompactError",
|
|
1346
|
-
"ParseMonthError",
|
|
1347
|
-
"SerializeCompactError",
|
|
1348
|
-
"SubDurationError",
|
|
1349
|
-
"TimedeltaToMillisecondsError",
|
|
1350
|
-
"YieldDaysError",
|
|
1351
|
-
"YieldWeekdaysError",
|
|
1352
|
-
"add_duration",
|
|
1353
|
-
"add_weekdays",
|
|
1354
|
-
"are_equal_date_durations",
|
|
1355
|
-
"are_equal_dates_or_datetimes",
|
|
1356
|
-
"are_equal_datetime_durations",
|
|
1357
|
-
"are_equal_datetimes",
|
|
1358
|
-
"are_equal_months",
|
|
1359
|
-
"check_date_not_datetime",
|
|
1360
|
-
"date_duration_to_int",
|
|
1361
|
-
"date_duration_to_timedelta",
|
|
1362
|
-
"date_to_datetime",
|
|
1363
|
-
"date_to_month",
|
|
1364
|
-
"datetime_duration_to_float",
|
|
1365
|
-
"datetime_duration_to_microseconds",
|
|
1366
|
-
"datetime_duration_to_milliseconds",
|
|
1367
|
-
"datetime_duration_to_timedelta",
|
|
1368
|
-
"datetime_utc",
|
|
1369
|
-
"days_since_epoch",
|
|
1370
|
-
"days_since_epoch_to_date",
|
|
1371
|
-
"ensure_month",
|
|
1372
|
-
"format_datetime_local_and_utc",
|
|
1373
|
-
"get_date",
|
|
1374
|
-
"get_datetime",
|
|
1375
|
-
"get_half_years",
|
|
1376
|
-
"get_min_max_date",
|
|
1377
|
-
"get_months",
|
|
1378
|
-
"get_now",
|
|
1379
|
-
"get_quarters",
|
|
1380
|
-
"get_today",
|
|
1381
|
-
"get_years",
|
|
1382
|
-
"is_integral_timedelta",
|
|
1383
|
-
"is_plain_datetime",
|
|
1384
|
-
"is_weekday",
|
|
1385
|
-
"is_zero_time",
|
|
1386
|
-
"is_zoned_datetime",
|
|
1387
|
-
"maybe_sub_pct_y",
|
|
1388
|
-
"mean_datetime",
|
|
1389
|
-
"mean_timedelta",
|
|
1390
|
-
"microseconds_since_epoch",
|
|
1391
|
-
"microseconds_since_epoch_to_datetime",
|
|
1392
|
-
"microseconds_to_timedelta",
|
|
1393
|
-
"milliseconds_since_epoch",
|
|
1394
|
-
"milliseconds_since_epoch_to_datetime",
|
|
1395
|
-
"milliseconds_to_timedelta",
|
|
1396
|
-
"parse_date_compact",
|
|
1397
|
-
"parse_datetime_compact",
|
|
1398
|
-
"parse_month",
|
|
1399
|
-
"parse_two_digit_year",
|
|
1400
|
-
"round_datetime",
|
|
1401
|
-
"round_to_next_weekday",
|
|
1402
|
-
"round_to_prev_weekday",
|
|
1403
|
-
"serialize_compact",
|
|
1404
|
-
"serialize_month",
|
|
1405
|
-
"sub_duration",
|
|
1406
|
-
"timedelta_since_epoch",
|
|
1407
|
-
"yield_days",
|
|
1408
|
-
"yield_weekdays",
|
|
1409
|
-
]
|