dycw-utilities 0.146.2__py3-none-any.whl → 0.178.1__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.
Potentially problematic release.
This version of dycw-utilities might be problematic. Click here for more details.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +129 -50
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +131 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/packaging.py +115 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +5 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +317 -153
- utilities/pottery.py +10 -86
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -51
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +100 -126
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +9 -4
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +59 -38
- utilities/types.py +68 -22
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +663 -178
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.146.2.dist-info/METADATA +0 -41
- dycw_utilities-0.146.2.dist-info/RECORD +0 -99
- dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
- dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- utilities/typed_settings.py +0 -144
utilities/whenever.py
CHANGED
|
@@ -10,12 +10,15 @@ from typing import (
|
|
|
10
10
|
TYPE_CHECKING,
|
|
11
11
|
Any,
|
|
12
12
|
Literal,
|
|
13
|
+
Self,
|
|
13
14
|
SupportsFloat,
|
|
15
|
+
TypedDict,
|
|
14
16
|
assert_never,
|
|
15
17
|
cast,
|
|
16
18
|
overload,
|
|
17
19
|
override,
|
|
18
20
|
)
|
|
21
|
+
from zoneinfo import ZoneInfo
|
|
19
22
|
|
|
20
23
|
from whenever import (
|
|
21
24
|
Date,
|
|
@@ -29,27 +32,28 @@ from whenever import (
|
|
|
29
32
|
ZonedDateTime,
|
|
30
33
|
)
|
|
31
34
|
|
|
35
|
+
from utilities.dataclasses import replace_non_sentinel
|
|
36
|
+
from utilities.functions import get_class_name
|
|
32
37
|
from utilities.math import sign
|
|
33
38
|
from utilities.platform import get_strftime
|
|
34
39
|
from utilities.sentinel import Sentinel, sentinel
|
|
35
40
|
from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
|
|
36
|
-
from utilities.zoneinfo import UTC,
|
|
41
|
+
from utilities.zoneinfo import UTC, to_time_zone_name
|
|
37
42
|
|
|
38
43
|
if TYPE_CHECKING:
|
|
39
|
-
from zoneinfo import ZoneInfo
|
|
40
|
-
|
|
41
44
|
from utilities.types import (
|
|
42
45
|
DateOrDateTimeDelta,
|
|
43
46
|
DateTimeRoundMode,
|
|
44
47
|
Delta,
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
MaybeCallableDateLike,
|
|
49
|
+
MaybeCallableTimeLike,
|
|
50
|
+
MaybeCallableZonedDateTimeLike,
|
|
47
51
|
TimeOrDateTimeDelta,
|
|
48
52
|
TimeZoneLike,
|
|
49
53
|
)
|
|
50
54
|
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
# bounds
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key)
|
|
@@ -142,6 +146,113 @@ def sub_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearM
|
|
|
142
146
|
##
|
|
143
147
|
|
|
144
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
|
+
|
|
168
|
+
@override
|
|
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())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(kw_only=True, slots=True)
|
|
244
|
+
class DatePeriodError(Exception):
|
|
245
|
+
start: Date
|
|
246
|
+
end: Date
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
def __str__(self) -> str:
|
|
250
|
+
return f"Invalid period; got {self.start} > {self.end}"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
##
|
|
254
|
+
|
|
255
|
+
|
|
145
256
|
def datetime_utc(
|
|
146
257
|
year: int,
|
|
147
258
|
month: int,
|
|
@@ -200,7 +311,11 @@ def diff_year_month(
|
|
|
200
311
|
|
|
201
312
|
|
|
202
313
|
def format_compact(
|
|
203
|
-
obj: Date | Time | PlainDateTime | ZonedDateTime,
|
|
314
|
+
obj: Date | Time | PlainDateTime | ZonedDateTime,
|
|
315
|
+
/,
|
|
316
|
+
*,
|
|
317
|
+
fmt: str | None = None,
|
|
318
|
+
path: bool = False,
|
|
204
319
|
) -> str:
|
|
205
320
|
"""Format the date/datetime in a compact fashion."""
|
|
206
321
|
match obj:
|
|
@@ -210,12 +325,16 @@ def format_compact(
|
|
|
210
325
|
case Time() as time:
|
|
211
326
|
obj_use = time.round().py_time()
|
|
212
327
|
fmt_use = "%H%M%S" if fmt is None else fmt
|
|
213
|
-
case PlainDateTime() as
|
|
214
|
-
obj_use =
|
|
328
|
+
case PlainDateTime() as date_time:
|
|
329
|
+
obj_use = date_time.round().py_datetime()
|
|
215
330
|
fmt_use = "%Y%m%dT%H%M%S" if fmt is None else fmt
|
|
216
|
-
case ZonedDateTime() as
|
|
217
|
-
|
|
218
|
-
|
|
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:
|
|
219
338
|
assert_never(never)
|
|
220
339
|
return obj_use.strftime(get_strftime(fmt_use))
|
|
221
340
|
|
|
@@ -225,52 +344,87 @@ def format_compact(
|
|
|
225
344
|
|
|
226
345
|
def from_timestamp(i: float, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
|
|
227
346
|
"""Get a zoned datetime from a timestamp."""
|
|
228
|
-
return ZonedDateTime.from_timestamp(i, tz=
|
|
347
|
+
return ZonedDateTime.from_timestamp(i, tz=to_time_zone_name(time_zone))
|
|
229
348
|
|
|
230
349
|
|
|
231
350
|
def from_timestamp_millis(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
|
|
232
351
|
"""Get a zoned datetime from a timestamp (in milliseconds)."""
|
|
233
|
-
return ZonedDateTime.from_timestamp_millis(i, tz=
|
|
352
|
+
return ZonedDateTime.from_timestamp_millis(i, tz=to_time_zone_name(time_zone))
|
|
234
353
|
|
|
235
354
|
|
|
236
355
|
def from_timestamp_nanos(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
|
|
237
356
|
"""Get a zoned datetime from a timestamp (in nanoseconds)."""
|
|
238
|
-
return ZonedDateTime.from_timestamp_nanos(i, tz=
|
|
357
|
+
return ZonedDateTime.from_timestamp_nanos(i, tz=to_time_zone_name(time_zone))
|
|
239
358
|
|
|
240
359
|
|
|
241
360
|
##
|
|
242
361
|
|
|
243
362
|
|
|
244
|
-
def get_now(
|
|
245
|
-
"""Get the current zoned
|
|
246
|
-
return ZonedDateTime.now(
|
|
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))
|
|
247
366
|
|
|
248
367
|
|
|
249
|
-
NOW_UTC = get_now(
|
|
368
|
+
NOW_UTC = get_now(UTC)
|
|
250
369
|
|
|
251
370
|
|
|
252
371
|
def get_now_local() -> ZonedDateTime:
|
|
253
|
-
"""Get the current local time."""
|
|
254
|
-
return get_now(
|
|
372
|
+
"""Get the current zoned date-time in the local time-zone."""
|
|
373
|
+
return get_now(LOCAL_TIME_ZONE)
|
|
255
374
|
|
|
256
375
|
|
|
257
376
|
NOW_LOCAL = get_now_local()
|
|
258
377
|
|
|
259
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
|
+
|
|
260
395
|
##
|
|
261
396
|
|
|
262
397
|
|
|
263
|
-
def
|
|
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
|
+
|
|
413
|
+
|
|
414
|
+
##
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_today(time_zone: TimeZoneLike = UTC, /) -> Date:
|
|
264
418
|
"""Get the current, timezone-aware local date."""
|
|
265
|
-
return get_now(time_zone
|
|
419
|
+
return get_now(time_zone).date()
|
|
266
420
|
|
|
267
421
|
|
|
268
|
-
TODAY_UTC = get_today(
|
|
422
|
+
TODAY_UTC = get_today(UTC)
|
|
269
423
|
|
|
270
424
|
|
|
271
425
|
def get_today_local() -> Date:
|
|
272
426
|
"""Get the current, timezone-aware local date."""
|
|
273
|
-
return get_today(
|
|
427
|
+
return get_today(LOCAL_TIME_ZONE)
|
|
274
428
|
|
|
275
429
|
|
|
276
430
|
TODAY_LOCAL = get_today_local()
|
|
@@ -279,6 +433,36 @@ TODAY_LOCAL = get_today_local()
|
|
|
279
433
|
##
|
|
280
434
|
|
|
281
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
|
+
|
|
282
466
|
def mean_datetime(
|
|
283
467
|
datetimes: Iterable[ZonedDateTime],
|
|
284
468
|
/,
|
|
@@ -316,20 +500,16 @@ def min_max_date(
|
|
|
316
500
|
max_age: DateDelta | None = None,
|
|
317
501
|
time_zone: TimeZoneLike = UTC,
|
|
318
502
|
) -> tuple[Date | None, Date | None]:
|
|
319
|
-
"""
|
|
320
|
-
today = get_today(time_zone
|
|
503
|
+
"""Compute the min/max date given a combination of dates/ages."""
|
|
504
|
+
today = get_today(time_zone)
|
|
321
505
|
min_parts: list[Date] = []
|
|
322
506
|
if min_date is not None:
|
|
323
|
-
if min_date > today:
|
|
324
|
-
raise _MinMaxDateMinDateError(min_date=min_date, today=today)
|
|
325
507
|
min_parts.append(min_date)
|
|
326
508
|
if max_age is not None:
|
|
327
509
|
min_parts.append(today - max_age)
|
|
328
510
|
min_date_use = max(min_parts, default=None)
|
|
329
511
|
max_parts: list[Date] = []
|
|
330
512
|
if max_date is not None:
|
|
331
|
-
if max_date > today:
|
|
332
|
-
raise _MinMaxDateMaxDateError(max_date=max_date, today=today)
|
|
333
513
|
max_parts.append(max_date)
|
|
334
514
|
if min_age is not None:
|
|
335
515
|
max_parts.append(today - min_age)
|
|
@@ -344,34 +524,13 @@ def min_max_date(
|
|
|
344
524
|
|
|
345
525
|
|
|
346
526
|
@dataclass(kw_only=True, slots=True)
|
|
347
|
-
class MinMaxDateError(Exception):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
@dataclass(kw_only=True, slots=True)
|
|
351
|
-
class _MinMaxDateMinDateError(MinMaxDateError):
|
|
527
|
+
class MinMaxDateError(Exception):
|
|
352
528
|
min_date: Date
|
|
353
|
-
today: Date
|
|
354
|
-
|
|
355
|
-
@override
|
|
356
|
-
def __str__(self) -> str:
|
|
357
|
-
return f"Min date must be at most today; got {self.min_date} > {self.today}"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
@dataclass(kw_only=True, slots=True)
|
|
361
|
-
class _MinMaxDateMaxDateError(MinMaxDateError):
|
|
362
529
|
max_date: Date
|
|
363
|
-
today: Date
|
|
364
|
-
|
|
365
|
-
@override
|
|
366
|
-
def __str__(self) -> str:
|
|
367
|
-
return f"Max date must be at most today; got {self.max_date} > {self.today}"
|
|
368
530
|
|
|
369
531
|
|
|
370
532
|
@dataclass(kw_only=True, slots=True)
|
|
371
533
|
class _MinMaxDatePeriodError(MinMaxDateError):
|
|
372
|
-
min_date: Date
|
|
373
|
-
max_date: Date
|
|
374
|
-
|
|
375
534
|
@override
|
|
376
535
|
def __str__(self) -> str:
|
|
377
536
|
return (
|
|
@@ -382,6 +541,18 @@ class _MinMaxDatePeriodError(MinMaxDateError):
|
|
|
382
541
|
##
|
|
383
542
|
|
|
384
543
|
|
|
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
|
+
|
|
385
556
|
type _RoundDateDailyUnit = Literal["W", "D"]
|
|
386
557
|
type _RoundDateTimeUnit = Literal["H", "M", "S", "ms", "us", "ns"]
|
|
387
558
|
type _RoundDateOrDateTimeUnit = _RoundDateDailyUnit | _RoundDateTimeUnit
|
|
@@ -422,7 +593,7 @@ def round_date_or_date_time[T: Date | PlainDateTime | ZonedDateTime](
|
|
|
422
593
|
raise _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(
|
|
423
594
|
date_time=date_time, delta=delta, weekday=weekday
|
|
424
595
|
)
|
|
425
|
-
case
|
|
596
|
+
case never:
|
|
426
597
|
assert_never(never)
|
|
427
598
|
|
|
428
599
|
|
|
@@ -516,7 +687,7 @@ def _round_date_weekly_or_daily(
|
|
|
516
687
|
return _round_date_daily(date, increment, mode=mode)
|
|
517
688
|
case "D", Weekday():
|
|
518
689
|
raise _RoundDateOrDateTimeDateWithWeekdayError(weekday=weekday)
|
|
519
|
-
case
|
|
690
|
+
case never:
|
|
520
691
|
assert_never(never)
|
|
521
692
|
|
|
522
693
|
|
|
@@ -562,7 +733,7 @@ def _round_date_daily(
|
|
|
562
733
|
threshold = increment // 2 + 1
|
|
563
734
|
case "half_ceil":
|
|
564
735
|
threshold = increment // 2 or 1
|
|
565
|
-
case
|
|
736
|
+
case never:
|
|
566
737
|
assert_never(never)
|
|
567
738
|
round_up = remainder >= threshold
|
|
568
739
|
return base.add(days=(quotient + round_up) * increment)
|
|
@@ -589,7 +760,7 @@ def _round_date_time_intraday[T: PlainDateTime | ZonedDateTime](
|
|
|
589
760
|
unit_use = "microsecond"
|
|
590
761
|
case "ns":
|
|
591
762
|
unit_use = "nanosecond"
|
|
592
|
-
case
|
|
763
|
+
case never:
|
|
593
764
|
assert_never(never)
|
|
594
765
|
return date_time.round(unit_use, increment=increment, mode=mode)
|
|
595
766
|
|
|
@@ -667,28 +838,96 @@ class _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(RoundDateOrDateTimeEr
|
|
|
667
838
|
##
|
|
668
839
|
|
|
669
840
|
|
|
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
|
+
|
|
670
903
|
@overload
|
|
671
|
-
def to_date(*,
|
|
672
|
-
@overload
|
|
673
|
-
def to_date(*, date: None) -> None: ...
|
|
674
|
-
@overload
|
|
675
|
-
def to_date(*, date: Sentinel) -> Sentinel: ...
|
|
676
|
-
@overload
|
|
677
|
-
def to_date(*, date: MaybeCallableDate | Sentinel) -> Date | Sentinel: ...
|
|
904
|
+
def to_date(date: Sentinel, /, *, time_zone: TimeZoneLike = UTC) -> Sentinel: ...
|
|
678
905
|
@overload
|
|
679
906
|
def to_date(
|
|
680
|
-
|
|
681
|
-
|
|
907
|
+
date: MaybeCallableDateLike | None | dt.date = get_today,
|
|
908
|
+
/,
|
|
909
|
+
*,
|
|
910
|
+
time_zone: TimeZoneLike = UTC,
|
|
911
|
+
) -> Date: ...
|
|
682
912
|
def to_date(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
913
|
+
date: MaybeCallableDateLike | dt.date | None | Sentinel = get_today,
|
|
914
|
+
/,
|
|
915
|
+
*,
|
|
916
|
+
time_zone: TimeZoneLike = UTC,
|
|
917
|
+
) -> Date | Sentinel:
|
|
918
|
+
"""Convert to a date."""
|
|
686
919
|
match date:
|
|
687
|
-
case Date() |
|
|
920
|
+
case Date() | Sentinel():
|
|
688
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)
|
|
689
928
|
case Callable() as func:
|
|
690
|
-
return to_date(
|
|
691
|
-
case
|
|
929
|
+
return to_date(func(), time_zone=time_zone)
|
|
930
|
+
case never:
|
|
692
931
|
assert_never(never)
|
|
693
932
|
|
|
694
933
|
|
|
@@ -755,7 +994,7 @@ def to_days(delta: Delta, /) -> int:
|
|
|
755
994
|
raise _ToDaysNanosecondsError(
|
|
756
995
|
delta=delta, nanoseconds=error.nanoseconds
|
|
757
996
|
) from None
|
|
758
|
-
case
|
|
997
|
+
case never:
|
|
759
998
|
assert_never(never)
|
|
760
999
|
|
|
761
1000
|
|
|
@@ -811,7 +1050,7 @@ def to_hours(delta: Delta, /) -> int:
|
|
|
811
1050
|
raise _ToHoursNanosecondsError(
|
|
812
1051
|
delta=delta, nanoseconds=error.nanoseconds
|
|
813
1052
|
) from None
|
|
814
|
-
case
|
|
1053
|
+
case never:
|
|
815
1054
|
assert_never(never)
|
|
816
1055
|
|
|
817
1056
|
|
|
@@ -842,14 +1081,6 @@ class _ToHoursNanosecondsError(ToHoursError):
|
|
|
842
1081
|
##
|
|
843
1082
|
|
|
844
1083
|
|
|
845
|
-
def to_local_plain(date_time: ZonedDateTime, /) -> PlainDateTime:
|
|
846
|
-
"""Convert a datetime to its local/plain variant."""
|
|
847
|
-
return date_time.to_tz(LOCAL_TIME_ZONE_NAME).to_plain()
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
##
|
|
851
|
-
|
|
852
|
-
|
|
853
1084
|
def to_microseconds(delta: Delta, /) -> int:
|
|
854
1085
|
"""Compute the number of microseconds in a delta."""
|
|
855
1086
|
match delta:
|
|
@@ -882,7 +1113,7 @@ def to_microseconds(delta: Delta, /) -> int:
|
|
|
882
1113
|
raise _ToMicrosecondsNanosecondsError(
|
|
883
1114
|
delta=delta, nanoseconds=error.nanoseconds
|
|
884
1115
|
) from None
|
|
885
|
-
case
|
|
1116
|
+
case never:
|
|
886
1117
|
assert_never(never)
|
|
887
1118
|
|
|
888
1119
|
|
|
@@ -945,7 +1176,7 @@ def to_milliseconds(delta: Delta, /) -> int:
|
|
|
945
1176
|
raise _ToMillisecondsNanosecondsError(
|
|
946
1177
|
delta=delta, nanoseconds=error.nanoseconds
|
|
947
1178
|
) from None
|
|
948
|
-
case
|
|
1179
|
+
case never:
|
|
949
1180
|
assert_never(never)
|
|
950
1181
|
|
|
951
1182
|
|
|
@@ -1000,7 +1231,7 @@ def to_minutes(delta: Delta, /) -> int:
|
|
|
1000
1231
|
raise _ToMinutesNanosecondsError(
|
|
1001
1232
|
delta=delta, nanoseconds=error.nanoseconds
|
|
1002
1233
|
) from None
|
|
1003
|
-
case
|
|
1234
|
+
case never:
|
|
1004
1235
|
assert_never(never)
|
|
1005
1236
|
|
|
1006
1237
|
|
|
@@ -1046,7 +1277,7 @@ def to_months(delta: DateOrDateTimeDelta, /) -> int:
|
|
|
1046
1277
|
return to_months(delta.date_part())
|
|
1047
1278
|
except _ToMonthsDaysError as error:
|
|
1048
1279
|
raise _ToMonthsDaysError(delta=delta, days=error.days) from None
|
|
1049
|
-
case
|
|
1280
|
+
case never:
|
|
1050
1281
|
assert_never(never)
|
|
1051
1282
|
|
|
1052
1283
|
|
|
@@ -1085,7 +1316,7 @@ def to_months_and_days(delta: DateOrDateTimeDelta, /) -> tuple[int, int]:
|
|
|
1085
1316
|
if delta.time_part() != TimeDelta():
|
|
1086
1317
|
raise ToMonthsAndDaysError(delta=delta)
|
|
1087
1318
|
return to_months_and_days(delta.date_part())
|
|
1088
|
-
case
|
|
1319
|
+
case never:
|
|
1089
1320
|
assert_never(never)
|
|
1090
1321
|
|
|
1091
1322
|
|
|
@@ -1119,7 +1350,7 @@ def to_nanoseconds(delta: Delta, /) -> int:
|
|
|
1119
1350
|
)
|
|
1120
1351
|
except ToNanosecondsError as error:
|
|
1121
1352
|
raise ToNanosecondsError(delta=delta, months=error.months) from None
|
|
1122
|
-
case
|
|
1353
|
+
case never:
|
|
1123
1354
|
assert_never(never)
|
|
1124
1355
|
|
|
1125
1356
|
|
|
@@ -1153,7 +1384,7 @@ def to_py_date_or_date_time(
|
|
|
1153
1384
|
return date_time.py_datetime()
|
|
1154
1385
|
case None:
|
|
1155
1386
|
return None
|
|
1156
|
-
case
|
|
1387
|
+
case never:
|
|
1157
1388
|
assert_never(never)
|
|
1158
1389
|
|
|
1159
1390
|
|
|
@@ -1181,7 +1412,7 @@ def to_py_time_delta(delta: Delta | None, /) -> dt.timedelta | None:
|
|
|
1181
1412
|
)
|
|
1182
1413
|
case None:
|
|
1183
1414
|
return None
|
|
1184
|
-
case
|
|
1415
|
+
case never:
|
|
1185
1416
|
assert_never(never)
|
|
1186
1417
|
|
|
1187
1418
|
|
|
@@ -1197,6 +1428,95 @@ class ToPyTimeDeltaError(Exception):
|
|
|
1197
1428
|
##
|
|
1198
1429
|
|
|
1199
1430
|
|
|
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): ...
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
@dataclass(kw_only=True, slots=True)
|
|
1464
|
+
class _ToSecondsMonthsError(ToSecondsError):
|
|
1465
|
+
delta: DateOrDateTimeDelta
|
|
1466
|
+
months: int
|
|
1467
|
+
|
|
1468
|
+
@override
|
|
1469
|
+
def __str__(self) -> str:
|
|
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}"
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
##
|
|
1484
|
+
|
|
1485
|
+
|
|
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)
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
##
|
|
1518
|
+
|
|
1519
|
+
|
|
1200
1520
|
def to_time_delta(nanos: int, /) -> TimeDelta:
|
|
1201
1521
|
"""Construct a time delta."""
|
|
1202
1522
|
components = _to_time_delta_components(nanos)
|
|
@@ -1275,61 +1595,6 @@ def _to_time_delta_components(nanos: int, /) -> _TimeDeltaComponents:
|
|
|
1275
1595
|
##
|
|
1276
1596
|
|
|
1277
1597
|
|
|
1278
|
-
def to_seconds(delta: Delta, /) -> int:
|
|
1279
|
-
"""Compute the number of seconds in a delta."""
|
|
1280
|
-
match delta:
|
|
1281
|
-
case DateDelta():
|
|
1282
|
-
try:
|
|
1283
|
-
days = to_days(delta)
|
|
1284
|
-
except _ToDaysMonthsError as error:
|
|
1285
|
-
raise _ToSecondsMonthsError(delta=delta, months=error.months) from None
|
|
1286
|
-
return 24 * 60 * 60 * days
|
|
1287
|
-
case TimeDelta():
|
|
1288
|
-
nanos = to_nanoseconds(delta)
|
|
1289
|
-
seconds, remainder = divmod(nanos, int(1e9))
|
|
1290
|
-
if remainder != 0:
|
|
1291
|
-
raise _ToSecondsNanosecondsError(delta=delta, nanoseconds=remainder)
|
|
1292
|
-
return seconds
|
|
1293
|
-
case DateTimeDelta():
|
|
1294
|
-
try:
|
|
1295
|
-
return to_seconds(delta.date_part()) + to_seconds(delta.time_part())
|
|
1296
|
-
except _ToSecondsMonthsError as error:
|
|
1297
|
-
raise _ToSecondsMonthsError(delta=delta, months=error.months) from None
|
|
1298
|
-
except _ToSecondsNanosecondsError as error:
|
|
1299
|
-
raise _ToSecondsNanosecondsError(
|
|
1300
|
-
delta=delta, nanoseconds=error.nanoseconds
|
|
1301
|
-
) from None
|
|
1302
|
-
case _ as never:
|
|
1303
|
-
assert_never(never)
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
@dataclass(kw_only=True, slots=True)
|
|
1307
|
-
class ToSecondsError(Exception): ...
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
@dataclass(kw_only=True, slots=True)
|
|
1311
|
-
class _ToSecondsMonthsError(ToSecondsError):
|
|
1312
|
-
delta: DateOrDateTimeDelta
|
|
1313
|
-
months: int
|
|
1314
|
-
|
|
1315
|
-
@override
|
|
1316
|
-
def __str__(self) -> str:
|
|
1317
|
-
return f"Delta must not contain months; got {self.months}"
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
@dataclass(kw_only=True, slots=True)
|
|
1321
|
-
class _ToSecondsNanosecondsError(ToSecondsError):
|
|
1322
|
-
delta: TimeOrDateTimeDelta
|
|
1323
|
-
nanoseconds: int
|
|
1324
|
-
|
|
1325
|
-
@override
|
|
1326
|
-
def __str__(self) -> str:
|
|
1327
|
-
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
##
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
1598
|
def to_weeks(delta: Delta, /) -> int:
|
|
1334
1599
|
"""Compute the number of weeks in a delta."""
|
|
1335
1600
|
try:
|
|
@@ -1402,7 +1667,7 @@ def to_years(delta: DateOrDateTimeDelta, /) -> int:
|
|
|
1402
1667
|
raise _ToYearsMonthsError(delta=delta, months=error.months) from None
|
|
1403
1668
|
except _ToYearsDaysError as error:
|
|
1404
1669
|
raise _ToYearsDaysError(delta=delta, days=error.days) from None
|
|
1405
|
-
case
|
|
1670
|
+
case never:
|
|
1406
1671
|
assert_never(never)
|
|
1407
1672
|
|
|
1408
1673
|
|
|
@@ -1443,22 +1708,56 @@ class _ToYearsTimeError(ToYearsError):
|
|
|
1443
1708
|
|
|
1444
1709
|
|
|
1445
1710
|
@overload
|
|
1446
|
-
def to_zoned_date_time(
|
|
1447
|
-
|
|
1448
|
-
|
|
1711
|
+
def to_zoned_date_time(
|
|
1712
|
+
date_time: Sentinel, /, *, time_zone: TimeZoneLike | None = None
|
|
1713
|
+
) -> Sentinel: ...
|
|
1449
1714
|
@overload
|
|
1450
|
-
def to_zoned_date_time(*, date_time: Sentinel) -> Sentinel: ...
|
|
1451
1715
|
def to_zoned_date_time(
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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."""
|
|
1455
1728
|
match date_time:
|
|
1456
|
-
case ZonedDateTime()
|
|
1457
|
-
|
|
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)
|
|
1458
1745
|
case Callable() as func:
|
|
1459
|
-
return to_zoned_date_time(
|
|
1460
|
-
case
|
|
1746
|
+
return to_zoned_date_time(func(), time_zone=time_zone)
|
|
1747
|
+
case never:
|
|
1461
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))
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@dataclass(kw_only=True, slots=True)
|
|
1755
|
+
class ToZonedDateTimeError(Exception):
|
|
1756
|
+
date_time: dt.datetime
|
|
1757
|
+
|
|
1758
|
+
@override
|
|
1759
|
+
def __str__(self) -> str:
|
|
1760
|
+
return f"Expected date-time to have a `ZoneInfo` or `dt.UTC` as its timezone; got {self.date_time.tzinfo}"
|
|
1462
1761
|
|
|
1463
1762
|
|
|
1464
1763
|
##
|
|
@@ -1498,32 +1797,203 @@ class WheneverLogRecord(LogRecord):
|
|
|
1498
1797
|
name, level, pathname, lineno, msg, args, exc_info, func, sinfo
|
|
1499
1798
|
)
|
|
1500
1799
|
length = self._get_length()
|
|
1501
|
-
plain = format(get_now_local().to_plain().
|
|
1502
|
-
|
|
1503
|
-
self.zoned_datetime = f"{plain}[{time_zone}]"
|
|
1504
|
-
|
|
1505
|
-
@classmethod
|
|
1506
|
-
@cache
|
|
1507
|
-
def _get_time_zone(cls) -> ZoneInfo:
|
|
1508
|
-
"""Get the local timezone."""
|
|
1509
|
-
try:
|
|
1510
|
-
from utilities.tzlocal import get_local_time_zone
|
|
1511
|
-
except ModuleNotFoundError: # pragma: no cover
|
|
1512
|
-
return UTC
|
|
1513
|
-
return get_local_time_zone()
|
|
1514
|
-
|
|
1515
|
-
@classmethod
|
|
1516
|
-
@cache
|
|
1517
|
-
def _get_time_zone_key(cls) -> str:
|
|
1518
|
-
"""Get the local timezone as a string."""
|
|
1519
|
-
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}]"
|
|
1520
1802
|
|
|
1521
1803
|
@classmethod
|
|
1522
1804
|
@cache
|
|
1523
1805
|
def _get_length(cls) -> int:
|
|
1524
1806
|
"""Get maximum length of a formatted string."""
|
|
1525
1807
|
now = get_now_local().replace(nanosecond=1000).to_plain()
|
|
1526
|
-
return len(now.
|
|
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')}"
|
|
1914
|
+
|
|
1915
|
+
@classmethod
|
|
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)
|
|
1944
|
+
|
|
1945
|
+
@property
|
|
1946
|
+
def time_zone(self) -> ZoneInfo:
|
|
1947
|
+
"""The time zone of the period."""
|
|
1948
|
+
return ZoneInfo(self.start.tz)
|
|
1949
|
+
|
|
1950
|
+
def to_dict(self) -> PeriodDict[ZonedDateTime]:
|
|
1951
|
+
"""Convert the period to a dictionary."""
|
|
1952
|
+
return PeriodDict(start=self.start, end=self.end)
|
|
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())
|
|
1957
|
+
|
|
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}"
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
@dataclass(kw_only=True, slots=True)
|
|
1991
|
+
class _ZonedDateTimePeriodExactEqError(ZonedDateTimePeriodError):
|
|
1992
|
+
args: tuple[Any, ...]
|
|
1993
|
+
|
|
1994
|
+
@override
|
|
1995
|
+
def __str__(self) -> str:
|
|
1996
|
+
return f"Invalid arguments; got {self.args}"
|
|
1527
1997
|
|
|
1528
1998
|
|
|
1529
1999
|
__all__ = [
|
|
@@ -1544,9 +2014,13 @@ __all__ = [
|
|
|
1544
2014
|
"MINUTE",
|
|
1545
2015
|
"MONTH",
|
|
1546
2016
|
"NOW_LOCAL",
|
|
2017
|
+
"NOW_LOCAL_PLAIN",
|
|
2018
|
+
"NOW_PLAIN",
|
|
1547
2019
|
"SECOND",
|
|
1548
2020
|
"TIME_DELTA_MAX",
|
|
1549
2021
|
"TIME_DELTA_MIN",
|
|
2022
|
+
"TIME_LOCAL",
|
|
2023
|
+
"TIME_UTC",
|
|
1550
2024
|
"TODAY_LOCAL",
|
|
1551
2025
|
"TODAY_UTC",
|
|
1552
2026
|
"WEEK",
|
|
@@ -1555,9 +2029,13 @@ __all__ = [
|
|
|
1555
2029
|
"ZERO_TIME",
|
|
1556
2030
|
"ZONED_DATE_TIME_MAX",
|
|
1557
2031
|
"ZONED_DATE_TIME_MIN",
|
|
2032
|
+
"DatePeriod",
|
|
2033
|
+
"DatePeriodError",
|
|
1558
2034
|
"MeanDateTimeError",
|
|
1559
2035
|
"MinMaxDateError",
|
|
2036
|
+
"PeriodDict",
|
|
1560
2037
|
"RoundDateOrDateTimeError",
|
|
2038
|
+
"TimePeriod",
|
|
1561
2039
|
"ToDaysError",
|
|
1562
2040
|
"ToMinutesError",
|
|
1563
2041
|
"ToMonthsAndDaysError",
|
|
@@ -1568,6 +2046,8 @@ __all__ = [
|
|
|
1568
2046
|
"ToWeeksError",
|
|
1569
2047
|
"ToYearsError",
|
|
1570
2048
|
"WheneverLogRecord",
|
|
2049
|
+
"ZonedDateTimePeriod",
|
|
2050
|
+
"ZonedDateTimePeriodError",
|
|
1571
2051
|
"add_year_month",
|
|
1572
2052
|
"datetime_utc",
|
|
1573
2053
|
"diff_year_month",
|
|
@@ -1577,8 +2057,13 @@ __all__ = [
|
|
|
1577
2057
|
"from_timestamp_nanos",
|
|
1578
2058
|
"get_now",
|
|
1579
2059
|
"get_now_local",
|
|
2060
|
+
"get_now_local_plain",
|
|
2061
|
+
"get_now_plain",
|
|
2062
|
+
"get_time",
|
|
2063
|
+
"get_time_local",
|
|
1580
2064
|
"get_today",
|
|
1581
2065
|
"get_today_local",
|
|
2066
|
+
"is_weekend",
|
|
1582
2067
|
"mean_datetime",
|
|
1583
2068
|
"min_max_date",
|
|
1584
2069
|
"round_date_or_date_time",
|
|
@@ -1586,7 +2071,6 @@ __all__ = [
|
|
|
1586
2071
|
"to_date",
|
|
1587
2072
|
"to_date_time_delta",
|
|
1588
2073
|
"to_days",
|
|
1589
|
-
"to_local_plain",
|
|
1590
2074
|
"to_microseconds",
|
|
1591
2075
|
"to_milliseconds",
|
|
1592
2076
|
"to_minutes",
|
|
@@ -1596,6 +2080,7 @@ __all__ = [
|
|
|
1596
2080
|
"to_py_date_or_date_time",
|
|
1597
2081
|
"to_py_time_delta",
|
|
1598
2082
|
"to_seconds",
|
|
2083
|
+
"to_time",
|
|
1599
2084
|
"to_weeks",
|
|
1600
2085
|
"to_years",
|
|
1601
2086
|
"to_zoned_date_time",
|