dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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.175.31.dist-info/METADATA +34 -0
- dycw_utilities-0.175.31.dist-info/RECORD +103 -0
- dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
- {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +113 -64
- 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 +381 -0
- utilities/enum.py +2 -2
- utilities/errors.py +1 -1
- 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 +12 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +74 -85
- 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/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +4 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +296 -174
- utilities/pottery.py +8 -73
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +155 -46
- 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 +8 -3
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1947 -0
- utilities/tempfile.py +95 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +2 -2
- utilities/traceback.py +46 -36
- utilities/types.py +62 -23
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +661 -151
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.148.5.dist-info/METADATA +0 -41
- dycw_utilities-0.148.5.dist-info/RECORD +0 -95
- dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
- dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- 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
|
/,
|
|
@@ -317,7 +501,7 @@ def min_max_date(
|
|
|
317
501
|
time_zone: TimeZoneLike = UTC,
|
|
318
502
|
) -> tuple[Date | None, Date | None]:
|
|
319
503
|
"""Compute the min/max date given a combination of dates/ages."""
|
|
320
|
-
today = get_today(time_zone
|
|
504
|
+
today = get_today(time_zone)
|
|
321
505
|
min_parts: list[Date] = []
|
|
322
506
|
if min_date is not None:
|
|
323
507
|
min_parts.append(min_date)
|
|
@@ -357,6 +541,18 @@ class _MinMaxDatePeriodError(MinMaxDateError):
|
|
|
357
541
|
##
|
|
358
542
|
|
|
359
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
|
+
|
|
360
556
|
type _RoundDateDailyUnit = Literal["W", "D"]
|
|
361
557
|
type _RoundDateTimeUnit = Literal["H", "M", "S", "ms", "us", "ns"]
|
|
362
558
|
type _RoundDateOrDateTimeUnit = _RoundDateDailyUnit | _RoundDateTimeUnit
|
|
@@ -397,7 +593,7 @@ def round_date_or_date_time[T: Date | PlainDateTime | ZonedDateTime](
|
|
|
397
593
|
raise _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(
|
|
398
594
|
date_time=date_time, delta=delta, weekday=weekday
|
|
399
595
|
)
|
|
400
|
-
case
|
|
596
|
+
case never:
|
|
401
597
|
assert_never(never)
|
|
402
598
|
|
|
403
599
|
|
|
@@ -491,7 +687,7 @@ def _round_date_weekly_or_daily(
|
|
|
491
687
|
return _round_date_daily(date, increment, mode=mode)
|
|
492
688
|
case "D", Weekday():
|
|
493
689
|
raise _RoundDateOrDateTimeDateWithWeekdayError(weekday=weekday)
|
|
494
|
-
case
|
|
690
|
+
case never:
|
|
495
691
|
assert_never(never)
|
|
496
692
|
|
|
497
693
|
|
|
@@ -537,7 +733,7 @@ def _round_date_daily(
|
|
|
537
733
|
threshold = increment // 2 + 1
|
|
538
734
|
case "half_ceil":
|
|
539
735
|
threshold = increment // 2 or 1
|
|
540
|
-
case
|
|
736
|
+
case never:
|
|
541
737
|
assert_never(never)
|
|
542
738
|
round_up = remainder >= threshold
|
|
543
739
|
return base.add(days=(quotient + round_up) * increment)
|
|
@@ -564,7 +760,7 @@ def _round_date_time_intraday[T: PlainDateTime | ZonedDateTime](
|
|
|
564
760
|
unit_use = "microsecond"
|
|
565
761
|
case "ns":
|
|
566
762
|
unit_use = "nanosecond"
|
|
567
|
-
case
|
|
763
|
+
case never:
|
|
568
764
|
assert_never(never)
|
|
569
765
|
return date_time.round(unit_use, increment=increment, mode=mode)
|
|
570
766
|
|
|
@@ -642,28 +838,96 @@ class _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(RoundDateOrDateTimeEr
|
|
|
642
838
|
##
|
|
643
839
|
|
|
644
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
|
+
|
|
645
903
|
@overload
|
|
646
|
-
def to_date(*,
|
|
647
|
-
@overload
|
|
648
|
-
def to_date(*, date: None) -> None: ...
|
|
649
|
-
@overload
|
|
650
|
-
def to_date(*, date: Sentinel) -> Sentinel: ...
|
|
651
|
-
@overload
|
|
652
|
-
def to_date(*, date: MaybeCallableDate | Sentinel) -> Date | Sentinel: ...
|
|
904
|
+
def to_date(date: Sentinel, /, *, time_zone: TimeZoneLike = UTC) -> Sentinel: ...
|
|
653
905
|
@overload
|
|
654
906
|
def to_date(
|
|
655
|
-
|
|
656
|
-
|
|
907
|
+
date: MaybeCallableDateLike | None | dt.date = get_today,
|
|
908
|
+
/,
|
|
909
|
+
*,
|
|
910
|
+
time_zone: TimeZoneLike = UTC,
|
|
911
|
+
) -> Date: ...
|
|
657
912
|
def to_date(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
913
|
+
date: MaybeCallableDateLike | dt.date | None | Sentinel = get_today,
|
|
914
|
+
/,
|
|
915
|
+
*,
|
|
916
|
+
time_zone: TimeZoneLike = UTC,
|
|
917
|
+
) -> Date | Sentinel:
|
|
918
|
+
"""Convert to a date."""
|
|
661
919
|
match date:
|
|
662
|
-
case Date() |
|
|
920
|
+
case Date() | Sentinel():
|
|
663
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)
|
|
664
928
|
case Callable() as func:
|
|
665
|
-
return to_date(
|
|
666
|
-
case
|
|
929
|
+
return to_date(func(), time_zone=time_zone)
|
|
930
|
+
case never:
|
|
667
931
|
assert_never(never)
|
|
668
932
|
|
|
669
933
|
|
|
@@ -730,7 +994,7 @@ def to_days(delta: Delta, /) -> int:
|
|
|
730
994
|
raise _ToDaysNanosecondsError(
|
|
731
995
|
delta=delta, nanoseconds=error.nanoseconds
|
|
732
996
|
) from None
|
|
733
|
-
case
|
|
997
|
+
case never:
|
|
734
998
|
assert_never(never)
|
|
735
999
|
|
|
736
1000
|
|
|
@@ -786,7 +1050,7 @@ def to_hours(delta: Delta, /) -> int:
|
|
|
786
1050
|
raise _ToHoursNanosecondsError(
|
|
787
1051
|
delta=delta, nanoseconds=error.nanoseconds
|
|
788
1052
|
) from None
|
|
789
|
-
case
|
|
1053
|
+
case never:
|
|
790
1054
|
assert_never(never)
|
|
791
1055
|
|
|
792
1056
|
|
|
@@ -817,14 +1081,6 @@ class _ToHoursNanosecondsError(ToHoursError):
|
|
|
817
1081
|
##
|
|
818
1082
|
|
|
819
1083
|
|
|
820
|
-
def to_local_plain(date_time: ZonedDateTime, /) -> PlainDateTime:
|
|
821
|
-
"""Convert a datetime to its local/plain variant."""
|
|
822
|
-
return date_time.to_tz(LOCAL_TIME_ZONE_NAME).to_plain()
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
##
|
|
826
|
-
|
|
827
|
-
|
|
828
1084
|
def to_microseconds(delta: Delta, /) -> int:
|
|
829
1085
|
"""Compute the number of microseconds in a delta."""
|
|
830
1086
|
match delta:
|
|
@@ -857,7 +1113,7 @@ def to_microseconds(delta: Delta, /) -> int:
|
|
|
857
1113
|
raise _ToMicrosecondsNanosecondsError(
|
|
858
1114
|
delta=delta, nanoseconds=error.nanoseconds
|
|
859
1115
|
) from None
|
|
860
|
-
case
|
|
1116
|
+
case never:
|
|
861
1117
|
assert_never(never)
|
|
862
1118
|
|
|
863
1119
|
|
|
@@ -920,7 +1176,7 @@ def to_milliseconds(delta: Delta, /) -> int:
|
|
|
920
1176
|
raise _ToMillisecondsNanosecondsError(
|
|
921
1177
|
delta=delta, nanoseconds=error.nanoseconds
|
|
922
1178
|
) from None
|
|
923
|
-
case
|
|
1179
|
+
case never:
|
|
924
1180
|
assert_never(never)
|
|
925
1181
|
|
|
926
1182
|
|
|
@@ -975,7 +1231,7 @@ def to_minutes(delta: Delta, /) -> int:
|
|
|
975
1231
|
raise _ToMinutesNanosecondsError(
|
|
976
1232
|
delta=delta, nanoseconds=error.nanoseconds
|
|
977
1233
|
) from None
|
|
978
|
-
case
|
|
1234
|
+
case never:
|
|
979
1235
|
assert_never(never)
|
|
980
1236
|
|
|
981
1237
|
|
|
@@ -1021,7 +1277,7 @@ def to_months(delta: DateOrDateTimeDelta, /) -> int:
|
|
|
1021
1277
|
return to_months(delta.date_part())
|
|
1022
1278
|
except _ToMonthsDaysError as error:
|
|
1023
1279
|
raise _ToMonthsDaysError(delta=delta, days=error.days) from None
|
|
1024
|
-
case
|
|
1280
|
+
case never:
|
|
1025
1281
|
assert_never(never)
|
|
1026
1282
|
|
|
1027
1283
|
|
|
@@ -1060,7 +1316,7 @@ def to_months_and_days(delta: DateOrDateTimeDelta, /) -> tuple[int, int]:
|
|
|
1060
1316
|
if delta.time_part() != TimeDelta():
|
|
1061
1317
|
raise ToMonthsAndDaysError(delta=delta)
|
|
1062
1318
|
return to_months_and_days(delta.date_part())
|
|
1063
|
-
case
|
|
1319
|
+
case never:
|
|
1064
1320
|
assert_never(never)
|
|
1065
1321
|
|
|
1066
1322
|
|
|
@@ -1094,7 +1350,7 @@ def to_nanoseconds(delta: Delta, /) -> int:
|
|
|
1094
1350
|
)
|
|
1095
1351
|
except ToNanosecondsError as error:
|
|
1096
1352
|
raise ToNanosecondsError(delta=delta, months=error.months) from None
|
|
1097
|
-
case
|
|
1353
|
+
case never:
|
|
1098
1354
|
assert_never(never)
|
|
1099
1355
|
|
|
1100
1356
|
|
|
@@ -1128,7 +1384,7 @@ def to_py_date_or_date_time(
|
|
|
1128
1384
|
return date_time.py_datetime()
|
|
1129
1385
|
case None:
|
|
1130
1386
|
return None
|
|
1131
|
-
case
|
|
1387
|
+
case never:
|
|
1132
1388
|
assert_never(never)
|
|
1133
1389
|
|
|
1134
1390
|
|
|
@@ -1156,7 +1412,7 @@ def to_py_time_delta(delta: Delta | None, /) -> dt.timedelta | None:
|
|
|
1156
1412
|
)
|
|
1157
1413
|
case None:
|
|
1158
1414
|
return None
|
|
1159
|
-
case
|
|
1415
|
+
case never:
|
|
1160
1416
|
assert_never(never)
|
|
1161
1417
|
|
|
1162
1418
|
|
|
@@ -1172,6 +1428,95 @@ class ToPyTimeDeltaError(Exception):
|
|
|
1172
1428
|
##
|
|
1173
1429
|
|
|
1174
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
|
+
|
|
1175
1520
|
def to_time_delta(nanos: int, /) -> TimeDelta:
|
|
1176
1521
|
"""Construct a time delta."""
|
|
1177
1522
|
components = _to_time_delta_components(nanos)
|
|
@@ -1250,61 +1595,6 @@ def _to_time_delta_components(nanos: int, /) -> _TimeDeltaComponents:
|
|
|
1250
1595
|
##
|
|
1251
1596
|
|
|
1252
1597
|
|
|
1253
|
-
def to_seconds(delta: Delta, /) -> int:
|
|
1254
|
-
"""Compute the number of seconds in a delta."""
|
|
1255
|
-
match delta:
|
|
1256
|
-
case DateDelta():
|
|
1257
|
-
try:
|
|
1258
|
-
days = to_days(delta)
|
|
1259
|
-
except _ToDaysMonthsError as error:
|
|
1260
|
-
raise _ToSecondsMonthsError(delta=delta, months=error.months) from None
|
|
1261
|
-
return 24 * 60 * 60 * days
|
|
1262
|
-
case TimeDelta():
|
|
1263
|
-
nanos = to_nanoseconds(delta)
|
|
1264
|
-
seconds, remainder = divmod(nanos, int(1e9))
|
|
1265
|
-
if remainder != 0:
|
|
1266
|
-
raise _ToSecondsNanosecondsError(delta=delta, nanoseconds=remainder)
|
|
1267
|
-
return seconds
|
|
1268
|
-
case DateTimeDelta():
|
|
1269
|
-
try:
|
|
1270
|
-
return to_seconds(delta.date_part()) + to_seconds(delta.time_part())
|
|
1271
|
-
except _ToSecondsMonthsError as error:
|
|
1272
|
-
raise _ToSecondsMonthsError(delta=delta, months=error.months) from None
|
|
1273
|
-
except _ToSecondsNanosecondsError as error:
|
|
1274
|
-
raise _ToSecondsNanosecondsError(
|
|
1275
|
-
delta=delta, nanoseconds=error.nanoseconds
|
|
1276
|
-
) from None
|
|
1277
|
-
case _ as never:
|
|
1278
|
-
assert_never(never)
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
@dataclass(kw_only=True, slots=True)
|
|
1282
|
-
class ToSecondsError(Exception): ...
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
@dataclass(kw_only=True, slots=True)
|
|
1286
|
-
class _ToSecondsMonthsError(ToSecondsError):
|
|
1287
|
-
delta: DateOrDateTimeDelta
|
|
1288
|
-
months: int
|
|
1289
|
-
|
|
1290
|
-
@override
|
|
1291
|
-
def __str__(self) -> str:
|
|
1292
|
-
return f"Delta must not contain months; got {self.months}"
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
@dataclass(kw_only=True, slots=True)
|
|
1296
|
-
class _ToSecondsNanosecondsError(ToSecondsError):
|
|
1297
|
-
delta: TimeOrDateTimeDelta
|
|
1298
|
-
nanoseconds: int
|
|
1299
|
-
|
|
1300
|
-
@override
|
|
1301
|
-
def __str__(self) -> str:
|
|
1302
|
-
return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
##
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
1598
|
def to_weeks(delta: Delta, /) -> int:
|
|
1309
1599
|
"""Compute the number of weeks in a delta."""
|
|
1310
1600
|
try:
|
|
@@ -1377,7 +1667,7 @@ def to_years(delta: DateOrDateTimeDelta, /) -> int:
|
|
|
1377
1667
|
raise _ToYearsMonthsError(delta=delta, months=error.months) from None
|
|
1378
1668
|
except _ToYearsDaysError as error:
|
|
1379
1669
|
raise _ToYearsDaysError(delta=delta, days=error.days) from None
|
|
1380
|
-
case
|
|
1670
|
+
case never:
|
|
1381
1671
|
assert_never(never)
|
|
1382
1672
|
|
|
1383
1673
|
|
|
@@ -1418,22 +1708,56 @@ class _ToYearsTimeError(ToYearsError):
|
|
|
1418
1708
|
|
|
1419
1709
|
|
|
1420
1710
|
@overload
|
|
1421
|
-
def to_zoned_date_time(
|
|
1422
|
-
|
|
1423
|
-
|
|
1711
|
+
def to_zoned_date_time(
|
|
1712
|
+
date_time: Sentinel, /, *, time_zone: TimeZoneLike | None = None
|
|
1713
|
+
) -> Sentinel: ...
|
|
1424
1714
|
@overload
|
|
1425
|
-
def to_zoned_date_time(*, date_time: Sentinel) -> Sentinel: ...
|
|
1426
1715
|
def to_zoned_date_time(
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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."""
|
|
1430
1728
|
match date_time:
|
|
1431
|
-
case ZonedDateTime()
|
|
1432
|
-
|
|
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)
|
|
1433
1745
|
case Callable() as func:
|
|
1434
|
-
return to_zoned_date_time(
|
|
1435
|
-
case
|
|
1746
|
+
return to_zoned_date_time(func(), time_zone=time_zone)
|
|
1747
|
+
case never:
|
|
1436
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}"
|
|
1437
1761
|
|
|
1438
1762
|
|
|
1439
1763
|
##
|
|
@@ -1473,32 +1797,203 @@ class WheneverLogRecord(LogRecord):
|
|
|
1473
1797
|
name, level, pathname, lineno, msg, args, exc_info, func, sinfo
|
|
1474
1798
|
)
|
|
1475
1799
|
length = self._get_length()
|
|
1476
|
-
plain = format(get_now_local().to_plain().
|
|
1477
|
-
|
|
1478
|
-
self.zoned_datetime = f"{plain}[{time_zone}]"
|
|
1479
|
-
|
|
1480
|
-
@classmethod
|
|
1481
|
-
@cache
|
|
1482
|
-
def _get_time_zone(cls) -> ZoneInfo:
|
|
1483
|
-
"""Get the local timezone."""
|
|
1484
|
-
try:
|
|
1485
|
-
from utilities.tzlocal import get_local_time_zone
|
|
1486
|
-
except ModuleNotFoundError: # pragma: no cover
|
|
1487
|
-
return UTC
|
|
1488
|
-
return get_local_time_zone()
|
|
1489
|
-
|
|
1490
|
-
@classmethod
|
|
1491
|
-
@cache
|
|
1492
|
-
def _get_time_zone_key(cls) -> str:
|
|
1493
|
-
"""Get the local timezone as a string."""
|
|
1494
|
-
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}]"
|
|
1495
1802
|
|
|
1496
1803
|
@classmethod
|
|
1497
1804
|
@cache
|
|
1498
1805
|
def _get_length(cls) -> int:
|
|
1499
1806
|
"""Get maximum length of a formatted string."""
|
|
1500
1807
|
now = get_now_local().replace(nanosecond=1000).to_plain()
|
|
1501
|
-
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}"
|
|
1502
1997
|
|
|
1503
1998
|
|
|
1504
1999
|
__all__ = [
|
|
@@ -1519,9 +2014,13 @@ __all__ = [
|
|
|
1519
2014
|
"MINUTE",
|
|
1520
2015
|
"MONTH",
|
|
1521
2016
|
"NOW_LOCAL",
|
|
2017
|
+
"NOW_LOCAL_PLAIN",
|
|
2018
|
+
"NOW_PLAIN",
|
|
1522
2019
|
"SECOND",
|
|
1523
2020
|
"TIME_DELTA_MAX",
|
|
1524
2021
|
"TIME_DELTA_MIN",
|
|
2022
|
+
"TIME_LOCAL",
|
|
2023
|
+
"TIME_UTC",
|
|
1525
2024
|
"TODAY_LOCAL",
|
|
1526
2025
|
"TODAY_UTC",
|
|
1527
2026
|
"WEEK",
|
|
@@ -1530,9 +2029,13 @@ __all__ = [
|
|
|
1530
2029
|
"ZERO_TIME",
|
|
1531
2030
|
"ZONED_DATE_TIME_MAX",
|
|
1532
2031
|
"ZONED_DATE_TIME_MIN",
|
|
2032
|
+
"DatePeriod",
|
|
2033
|
+
"DatePeriodError",
|
|
1533
2034
|
"MeanDateTimeError",
|
|
1534
2035
|
"MinMaxDateError",
|
|
2036
|
+
"PeriodDict",
|
|
1535
2037
|
"RoundDateOrDateTimeError",
|
|
2038
|
+
"TimePeriod",
|
|
1536
2039
|
"ToDaysError",
|
|
1537
2040
|
"ToMinutesError",
|
|
1538
2041
|
"ToMonthsAndDaysError",
|
|
@@ -1543,6 +2046,8 @@ __all__ = [
|
|
|
1543
2046
|
"ToWeeksError",
|
|
1544
2047
|
"ToYearsError",
|
|
1545
2048
|
"WheneverLogRecord",
|
|
2049
|
+
"ZonedDateTimePeriod",
|
|
2050
|
+
"ZonedDateTimePeriodError",
|
|
1546
2051
|
"add_year_month",
|
|
1547
2052
|
"datetime_utc",
|
|
1548
2053
|
"diff_year_month",
|
|
@@ -1552,8 +2057,13 @@ __all__ = [
|
|
|
1552
2057
|
"from_timestamp_nanos",
|
|
1553
2058
|
"get_now",
|
|
1554
2059
|
"get_now_local",
|
|
2060
|
+
"get_now_local_plain",
|
|
2061
|
+
"get_now_plain",
|
|
2062
|
+
"get_time",
|
|
2063
|
+
"get_time_local",
|
|
1555
2064
|
"get_today",
|
|
1556
2065
|
"get_today_local",
|
|
2066
|
+
"is_weekend",
|
|
1557
2067
|
"mean_datetime",
|
|
1558
2068
|
"min_max_date",
|
|
1559
2069
|
"round_date_or_date_time",
|
|
@@ -1561,7 +2071,6 @@ __all__ = [
|
|
|
1561
2071
|
"to_date",
|
|
1562
2072
|
"to_date_time_delta",
|
|
1563
2073
|
"to_days",
|
|
1564
|
-
"to_local_plain",
|
|
1565
2074
|
"to_microseconds",
|
|
1566
2075
|
"to_milliseconds",
|
|
1567
2076
|
"to_minutes",
|
|
@@ -1571,6 +2080,7 @@ __all__ = [
|
|
|
1571
2080
|
"to_py_date_or_date_time",
|
|
1572
2081
|
"to_py_time_delta",
|
|
1573
2082
|
"to_seconds",
|
|
2083
|
+
"to_time",
|
|
1574
2084
|
"to_weeks",
|
|
1575
2085
|
"to_years",
|
|
1576
2086
|
"to_zoned_date_time",
|