dycw-utilities 0.148.4__py3-none-any.whl → 0.174.12__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.

Files changed (83) hide show
  1. dycw_utilities-0.174.12.dist-info/METADATA +41 -0
  2. dycw_utilities-0.174.12.dist-info/RECORD +104 -0
  3. dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
  5. utilities/__init__.py +1 -1
  6. utilities/{eventkit.py → aeventkit.py} +12 -11
  7. utilities/altair.py +7 -6
  8. utilities/asyncio.py +113 -64
  9. utilities/atomicwrites.py +1 -1
  10. utilities/atools.py +64 -4
  11. utilities/cachetools.py +9 -6
  12. utilities/click.py +145 -49
  13. utilities/concurrent.py +1 -1
  14. utilities/contextlib.py +4 -2
  15. utilities/contextvars.py +20 -1
  16. utilities/cryptography.py +3 -3
  17. utilities/dataclasses.py +15 -28
  18. utilities/docker.py +292 -0
  19. utilities/enum.py +2 -2
  20. utilities/errors.py +1 -1
  21. utilities/fastapi.py +8 -3
  22. utilities/fpdf2.py +2 -2
  23. utilities/functions.py +20 -297
  24. utilities/git.py +19 -0
  25. utilities/grp.py +28 -0
  26. utilities/hypothesis.py +360 -78
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +12 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +74 -85
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/parse.py +2 -2
  39. utilities/pathlib.py +66 -34
  40. utilities/permissions.py +297 -0
  41. utilities/platform.py +5 -5
  42. utilities/polars.py +932 -420
  43. utilities/polars_ols.py +1 -1
  44. utilities/postgres.py +299 -174
  45. utilities/pottery.py +8 -73
  46. utilities/pqdm.py +3 -3
  47. utilities/pwd.py +28 -0
  48. utilities/pydantic.py +11 -0
  49. utilities/pydantic_settings.py +240 -0
  50. utilities/pydantic_settings_sops.py +76 -0
  51. utilities/pyinstrument.py +5 -5
  52. utilities/pytest.py +155 -46
  53. utilities/pytest_plugins/pytest_randomly.py +1 -1
  54. utilities/pytest_plugins/pytest_regressions.py +7 -3
  55. utilities/pytest_regressions.py +2 -3
  56. utilities/random.py +11 -6
  57. utilities/re.py +1 -1
  58. utilities/redis.py +101 -64
  59. utilities/sentinel.py +10 -0
  60. utilities/shelve.py +4 -1
  61. utilities/shutil.py +25 -0
  62. utilities/slack_sdk.py +8 -3
  63. utilities/sqlalchemy.py +422 -352
  64. utilities/sqlalchemy_polars.py +28 -52
  65. utilities/string.py +1 -1
  66. utilities/subprocess.py +864 -0
  67. utilities/tempfile.py +62 -4
  68. utilities/testbook.py +50 -0
  69. utilities/text.py +165 -42
  70. utilities/timer.py +2 -2
  71. utilities/traceback.py +46 -36
  72. utilities/types.py +62 -23
  73. utilities/typing.py +479 -19
  74. utilities/uuid.py +42 -5
  75. utilities/version.py +27 -26
  76. utilities/whenever.py +661 -151
  77. utilities/zoneinfo.py +80 -22
  78. dycw_utilities-0.148.4.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.4.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.4.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.4.dist-info/licenses/LICENSE +0 -21
  82. utilities/period.py +0 -237
  83. 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, get_time_zone_name
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
- MaybeCallableDate,
46
- MaybeCallableZonedDateTime,
48
+ MaybeCallableDateLike,
49
+ MaybeCallableTimeLike,
50
+ MaybeCallableZonedDateTimeLike,
47
51
  TimeOrDateTimeDelta,
48
52
  TimeZoneLike,
49
53
  )
50
54
 
51
55
 
52
- ## bounds
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, /, *, fmt: str | None = None
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 datetime:
214
- obj_use = datetime.round().py_datetime()
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 datetime:
217
- return f"{format_compact(datetime.to_plain(), fmt=fmt)}[{datetime.tz}]"
218
- case _ as never:
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=get_time_zone_name(time_zone))
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=get_time_zone_name(time_zone))
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=get_time_zone_name(time_zone))
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(*, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
245
- """Get the current zoned datetime."""
246
- return ZonedDateTime.now(get_time_zone_name(time_zone))
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(time_zone=UTC)
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(time_zone=LOCAL_TIME_ZONE)
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 get_today(*, time_zone: TimeZoneLike = UTC) -> Date:
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=time_zone).date()
419
+ return get_now(time_zone).date()
266
420
 
267
421
 
268
- TODAY_UTC = get_today(time_zone=UTC)
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(time_zone=LOCAL_TIME_ZONE)
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=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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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(*, date: MaybeCallableDate) -> 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
- *, date: MaybeCallableDate | None | Sentinel = sentinel
656
- ) -> Date | None | Sentinel: ...
907
+ date: MaybeCallableDateLike | None | dt.date = get_today,
908
+ /,
909
+ *,
910
+ time_zone: TimeZoneLike = UTC,
911
+ ) -> Date: ...
657
912
  def to_date(
658
- *, date: MaybeCallableDate | None | Sentinel = sentinel
659
- ) -> Date | None | Sentinel:
660
- """Get the date."""
913
+ date: MaybeCallableDateLike | dt.date | None | Sentinel = get_today,
914
+ /,
915
+ *,
916
+ time_zone: TimeZoneLike = UTC,
917
+ ) -> Date | Sentinel:
918
+ """Convert to a date."""
661
919
  match date:
662
- case Date() | None | Sentinel():
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(date=func())
666
- case _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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 _ as never:
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(*, date_time: MaybeCallableZonedDateTime) -> ZonedDateTime: ...
1422
- @overload
1423
- def to_zoned_date_time(*, date_time: None) -> None: ...
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
- *, date_time: MaybeCallableZonedDateTime | None | Sentinel = sentinel
1428
- ) -> ZonedDateTime | None | Sentinel:
1429
- """Resolve into a zoned date_time."""
1716
+ date_time: MaybeCallableZonedDateTimeLike | dt.datetime | None = get_now,
1717
+ /,
1718
+ *,
1719
+ time_zone: TimeZoneLike | None = None,
1720
+ ) -> ZonedDateTime: ...
1721
+ def to_zoned_date_time(
1722
+ date_time: MaybeCallableZonedDateTimeLike | dt.datetime | None | Sentinel = get_now,
1723
+ /,
1724
+ *,
1725
+ time_zone: TimeZoneLike | None = None,
1726
+ ) -> ZonedDateTime | Sentinel:
1727
+ """Convert to a zoned date-time."""
1430
1728
  match date_time:
1431
- case ZonedDateTime() | None | Sentinel():
1432
- return date_time
1729
+ case ZonedDateTime() as date_time_use:
1730
+ ...
1731
+ case Sentinel():
1732
+ return sentinel
1733
+ case None:
1734
+ return get_now(UTC if time_zone is None else time_zone)
1735
+ case str() as text:
1736
+ date_time_use = ZonedDateTime.parse_iso(text.replace("~", "/"))
1737
+ case dt.datetime() as py_date_time:
1738
+ if isinstance(date_time.tzinfo, ZoneInfo):
1739
+ py_date_time_use = py_date_time
1740
+ elif date_time.tzinfo is dt.UTC:
1741
+ py_date_time_use = py_date_time.astimezone(UTC)
1742
+ else:
1743
+ raise ToZonedDateTimeError(date_time=date_time)
1744
+ date_time_use = ZonedDateTime.from_py_datetime(py_date_time_use)
1433
1745
  case Callable() as func:
1434
- return to_zoned_date_time(date_time=func())
1435
- case _ as never:
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().format_common_iso(), f"{length}s")
1477
- time_zone = self._get_time_zone_key()
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.format_common_iso())
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",