dycw-utilities 0.151.12__py3-none-any.whl → 0.153.0__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.
utilities/whenever.py CHANGED
@@ -10,7 +10,10 @@ from typing import (
10
10
  TYPE_CHECKING,
11
11
  Any,
12
12
  Literal,
13
+ Self,
13
14
  SupportsFloat,
15
+ TypedDict,
16
+ TypeVar,
14
17
  assert_never,
15
18
  cast,
16
19
  overload,
@@ -30,25 +33,29 @@ from whenever import (
30
33
  ZonedDateTime,
31
34
  )
32
35
 
36
+ from utilities.dataclasses import replace_non_sentinel
37
+ from utilities.functions import get_class_name
33
38
  from utilities.math import sign
34
39
  from utilities.platform import get_strftime
35
40
  from utilities.sentinel import Sentinel, sentinel
36
41
  from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
37
- from utilities.zoneinfo import UTC, get_time_zone_name
42
+ from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
38
43
 
39
44
  if TYPE_CHECKING:
40
45
  from utilities.types import (
41
46
  DateOrDateTimeDelta,
42
47
  DateTimeRoundMode,
43
48
  Delta,
44
- MaybeCallableDate,
45
- MaybeCallableZonedDateTime,
49
+ MaybeCallableDateLike,
50
+ MaybeCallableZonedDateTimeLike,
46
51
  TimeOrDateTimeDelta,
47
52
  TimeZoneLike,
48
53
  )
49
54
 
55
+ # type vars
50
56
 
51
- ## bounds
57
+
58
+ # bounds
52
59
 
53
60
 
54
61
  ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key)
@@ -141,6 +148,112 @@ def sub_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearM
141
148
  ##
142
149
 
143
150
 
151
+ _TDate_co = TypeVar("_TDate_co", bound=Date | dt.date, covariant=True)
152
+
153
+
154
+ @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
155
+ class DatePeriod:
156
+ """A period of dates."""
157
+
158
+ start: Date
159
+ end: Date
160
+
161
+ def __post_init__(self) -> None:
162
+ if self.start > self.end:
163
+ raise DatePeriodError(start=self.start, end=self.end)
164
+
165
+ def __add__(self, other: DateDelta, /) -> Self:
166
+ """Offset the period."""
167
+ return self.replace(start=self.start + other, end=self.end + other)
168
+
169
+ def __contains__(self, other: Date, /) -> bool:
170
+ """Check if a date/datetime lies in the period."""
171
+ return self.start <= other <= self.end
172
+
173
+ @override
174
+ def __repr__(self) -> str:
175
+ cls = get_class_name(self)
176
+ return f"{cls}({self.start}, {self.end})"
177
+
178
+ def __sub__(self, other: DateDelta, /) -> Self:
179
+ """Offset the period."""
180
+ return self.replace(start=self.start - other, end=self.end - other)
181
+
182
+ def at(
183
+ self, obj: Time | tuple[Time, Time], /, *, time_zone: TimeZoneLike = UTC
184
+ ) -> ZonedDateTimePeriod:
185
+ """Combine a date with a time to create a datetime."""
186
+ match obj:
187
+ case Time() as time:
188
+ start = end = time
189
+ case Time() as start, Time() as end:
190
+ ...
191
+ case never:
192
+ assert_never(never)
193
+ tz = ensure_time_zone(time_zone).key
194
+ return ZonedDateTimePeriod(
195
+ self.start.at(start).assume_tz(tz), self.end.at(end).assume_tz(tz)
196
+ )
197
+
198
+ @property
199
+ def delta(self) -> DateDelta:
200
+ """The delta of the period."""
201
+ return self.end - self.start
202
+
203
+ def format_compact(self) -> str:
204
+ """Format the period in a compact fashion."""
205
+ fc, start, end = format_compact, self.start, self.end
206
+ if self.start == self.end:
207
+ return f"{fc(start)}="
208
+ if self.start.year_month() == self.end.year_month():
209
+ return f"{fc(start)}-{fc(end, fmt='%d')}"
210
+ if self.start.year == self.end.year:
211
+ return f"{fc(start)}-{fc(end, fmt='%m%d')}"
212
+ return f"{fc(start)}-{fc(end)}"
213
+
214
+ @classmethod
215
+ def from_dict(cls, mapping: PeriodDict[_TDate_co], /) -> Self:
216
+ """Convert the dictionary to a period."""
217
+ match mapping["start"]:
218
+ case Date() as start:
219
+ ...
220
+ case dt.date() as py_date:
221
+ start = Date.from_py_date(py_date)
222
+ case never:
223
+ assert_never(never)
224
+ match mapping["end"]:
225
+ case Date() as end:
226
+ ...
227
+ case dt.date() as py_date:
228
+ end = Date.from_py_date(py_date)
229
+ case never:
230
+ assert_never(never)
231
+ return cls(start=start, end=end)
232
+
233
+ def replace(
234
+ self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
235
+ ) -> Self:
236
+ """Replace elements of the period."""
237
+ return replace_non_sentinel(self, start=start, end=end)
238
+
239
+ def to_dict(self) -> PeriodDict[Date]:
240
+ """Convert the period to a dictionary."""
241
+ return PeriodDict(start=self.start, end=self.end)
242
+
243
+
244
+ @dataclass(kw_only=True, slots=True)
245
+ class DatePeriodError(Exception):
246
+ start: Date
247
+ end: Date
248
+
249
+ @override
250
+ def __str__(self) -> str:
251
+ return f"Invalid period; got {self.start} > {self.end}"
252
+
253
+
254
+ ##
255
+
256
+
144
257
  def datetime_utc(
145
258
  year: int,
146
259
  month: int,
@@ -240,17 +353,17 @@ def from_timestamp_nanos(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDa
240
353
  ##
241
354
 
242
355
 
243
- def get_now(*, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
356
+ def get_now(time_zone: TimeZoneLike = UTC, /) -> ZonedDateTime:
244
357
  """Get the current zoned datetime."""
245
358
  return ZonedDateTime.now(get_time_zone_name(time_zone))
246
359
 
247
360
 
248
- NOW_UTC = get_now(time_zone=UTC)
361
+ NOW_UTC = get_now(UTC)
249
362
 
250
363
 
251
364
  def get_now_local() -> ZonedDateTime:
252
365
  """Get the current local time."""
253
- return get_now(time_zone=LOCAL_TIME_ZONE)
366
+ return get_now(LOCAL_TIME_ZONE)
254
367
 
255
368
 
256
369
  NOW_LOCAL = get_now_local()
@@ -259,17 +372,17 @@ NOW_LOCAL = get_now_local()
259
372
  ##
260
373
 
261
374
 
262
- def get_today(*, time_zone: TimeZoneLike = UTC) -> Date:
375
+ def get_today(time_zone: TimeZoneLike = UTC, /) -> Date:
263
376
  """Get the current, timezone-aware local date."""
264
- return get_now(time_zone=time_zone).date()
377
+ return get_now(time_zone).date()
265
378
 
266
379
 
267
- TODAY_UTC = get_today(time_zone=UTC)
380
+ TODAY_UTC = get_today(UTC)
268
381
 
269
382
 
270
383
  def get_today_local() -> Date:
271
384
  """Get the current, timezone-aware local date."""
272
- return get_today(time_zone=LOCAL_TIME_ZONE)
385
+ return get_today(LOCAL_TIME_ZONE)
273
386
 
274
387
 
275
388
  TODAY_LOCAL = get_today_local()
@@ -316,7 +429,7 @@ def min_max_date(
316
429
  time_zone: TimeZoneLike = UTC,
317
430
  ) -> tuple[Date | None, Date | None]:
318
431
  """Compute the min/max date given a combination of dates/ages."""
319
- today = get_today(time_zone=time_zone)
432
+ today = get_today(time_zone)
320
433
  min_parts: list[Date] = []
321
434
  if min_date is not None:
322
435
  min_parts.append(min_date)
@@ -356,6 +469,18 @@ class _MinMaxDatePeriodError(MinMaxDateError):
356
469
  ##
357
470
 
358
471
 
472
+ class PeriodDict[T: Date | Time | ZonedDateTime | dt.date | dt.time | dt.datetime](
473
+ TypedDict
474
+ ):
475
+ """A period as a dictionary."""
476
+
477
+ start: T
478
+ end: T
479
+
480
+
481
+ ##
482
+
483
+
359
484
  type _RoundDateDailyUnit = Literal["W", "D"]
360
485
  type _RoundDateTimeUnit = Literal["H", "M", "S", "ms", "us", "ns"]
361
486
  type _RoundDateOrDateTimeUnit = _RoundDateDailyUnit | _RoundDateTimeUnit
@@ -641,27 +766,94 @@ class _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(RoundDateOrDateTimeEr
641
766
  ##
642
767
 
643
768
 
769
+ _TTime_co = TypeVar("_TTime_co", bound=Time | dt.time, covariant=True)
770
+
771
+
772
+ @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
773
+ class TimePeriod:
774
+ """A period of times."""
775
+
776
+ start: Time
777
+ end: Time
778
+
779
+ @override
780
+ def __repr__(self) -> str:
781
+ cls = get_class_name(self)
782
+ return f"{cls}({self.start}, {self.end})"
783
+
784
+ def at(
785
+ self, obj: Date | tuple[Date, Date], /, *, time_zone: TimeZoneLike = UTC
786
+ ) -> ZonedDateTimePeriod:
787
+ """Combine a date with a time to create a datetime."""
788
+ match obj:
789
+ case Date() as date:
790
+ start = end = date
791
+ case Date() as start, Date() as end:
792
+ ...
793
+ case never:
794
+ assert_never(never)
795
+ return DatePeriod(start, end).at((self.start, self.end), time_zone=time_zone)
796
+
797
+ @classmethod
798
+ def from_dict(cls, mapping: PeriodDict[_TTime_co], /) -> Self:
799
+ """Convert the dictionary to a period."""
800
+ match mapping["start"]:
801
+ case Time() as start:
802
+ ...
803
+ case dt.time() as py_time:
804
+ start = Time.from_py_time(py_time)
805
+ case never:
806
+ assert_never(never)
807
+ match mapping["end"]:
808
+ case Time() as end:
809
+ ...
810
+ case dt.time() as py_time:
811
+ end = Time.from_py_time(py_time)
812
+ case never:
813
+ assert_never(never)
814
+ return cls(start=start, end=end)
815
+
816
+ def replace(
817
+ self, *, start: Time | Sentinel = sentinel, end: Time | Sentinel = sentinel
818
+ ) -> Self:
819
+ """Replace elements of the period."""
820
+ return replace_non_sentinel(self, start=start, end=end)
821
+
822
+ def to_dict(self) -> PeriodDict[Time]:
823
+ """Convert the period to a dictionary."""
824
+ return PeriodDict(start=self.start, end=self.end)
825
+
826
+
827
+ ##
828
+
829
+
644
830
  @overload
645
- def to_date(*, date: MaybeCallableDate) -> Date: ...
646
- @overload
647
- def to_date(*, date: None) -> None: ...
648
- @overload
649
- def to_date(*, date: Sentinel) -> Sentinel: ...
650
- @overload
651
- def to_date(*, date: MaybeCallableDate | Sentinel) -> Date | Sentinel: ...
831
+ def to_date(date: Sentinel, /, *, time_zone: TimeZoneLike = UTC) -> Sentinel: ...
652
832
  @overload
653
833
  def to_date(
654
- *, date: MaybeCallableDate | None | Sentinel = sentinel
655
- ) -> Date | None | Sentinel: ...
834
+ date: MaybeCallableDateLike | None | dt.date = get_today,
835
+ /,
836
+ *,
837
+ time_zone: TimeZoneLike = UTC,
838
+ ) -> Date: ...
656
839
  def to_date(
657
- *, date: MaybeCallableDate | None | Sentinel = sentinel
658
- ) -> Date | None | Sentinel:
659
- """Get the date."""
840
+ date: MaybeCallableDateLike | dt.date | None | Sentinel = get_today,
841
+ /,
842
+ *,
843
+ time_zone: TimeZoneLike = UTC,
844
+ ) -> Date | Sentinel:
845
+ """Convert to a date."""
660
846
  match date:
661
- case Date() | None | Sentinel():
847
+ case Date() | Sentinel():
662
848
  return date
849
+ case None:
850
+ return get_today(time_zone)
851
+ case str():
852
+ return Date.parse_common_iso(date)
853
+ case dt.date():
854
+ return Date.from_py_date(date)
663
855
  case Callable() as func:
664
- return to_date(date=func())
856
+ return to_date(func(), time_zone=time_zone)
665
857
  case never:
666
858
  assert_never(never)
667
859
 
@@ -1418,19 +1610,29 @@ class _ToYearsTimeError(ToYearsError):
1418
1610
 
1419
1611
  @overload
1420
1612
  def to_zoned_date_time(
1421
- *, date_time: MaybeCallableZonedDateTime | dt.datetime
1422
- ) -> ZonedDateTime: ...
1423
- @overload
1424
- def to_zoned_date_time(*, date_time: None) -> None: ...
1613
+ date_time: Sentinel, /, *, time_zone: TimeZoneLike = UTC
1614
+ ) -> Sentinel: ...
1425
1615
  @overload
1426
- def to_zoned_date_time(*, date_time: Sentinel) -> Sentinel: ...
1427
1616
  def to_zoned_date_time(
1428
- *, date_time: MaybeCallableZonedDateTime | dt.datetime | None | Sentinel = sentinel
1429
- ) -> ZonedDateTime | None | Sentinel:
1430
- """Resolve into a zoned date_time."""
1617
+ date_time: MaybeCallableZonedDateTimeLike | dt.datetime | None = get_now,
1618
+ /,
1619
+ *,
1620
+ time_zone: TimeZoneLike = UTC,
1621
+ ) -> ZonedDateTime: ...
1622
+ def to_zoned_date_time(
1623
+ date_time: MaybeCallableZonedDateTimeLike | dt.datetime | None | Sentinel = get_now,
1624
+ /,
1625
+ *,
1626
+ time_zone: TimeZoneLike = UTC,
1627
+ ) -> ZonedDateTime | Sentinel:
1628
+ """Convert to a zoned date-time."""
1431
1629
  match date_time:
1432
- case ZonedDateTime() | None | Sentinel():
1630
+ case ZonedDateTime() | Sentinel():
1433
1631
  return date_time
1632
+ case None:
1633
+ return get_now(time_zone)
1634
+ case str():
1635
+ return ZonedDateTime.parse_common_iso(date_time)
1434
1636
  case dt.datetime():
1435
1637
  if isinstance(date_time.tzinfo, ZoneInfo):
1436
1638
  return ZonedDateTime.from_py_datetime(date_time)
@@ -1438,10 +1640,9 @@ def to_zoned_date_time(
1438
1640
  return ZonedDateTime.from_py_datetime(date_time.astimezone(UTC))
1439
1641
  raise ToZonedDateTimeError(date_time=date_time)
1440
1642
  case Callable() as func:
1441
- return to_zoned_date_time(date_time=func())
1643
+ return to_zoned_date_time(func(), time_zone=time_zone)
1442
1644
  case never:
1443
1645
  assert_never(never)
1444
- return None
1445
1646
 
1446
1647
 
1447
1648
  @dataclass(kw_only=True, slots=True)
@@ -1518,6 +1719,193 @@ class WheneverLogRecord(LogRecord):
1518
1719
  return len(now.format_common_iso())
1519
1720
 
1520
1721
 
1722
+ ##
1723
+
1724
+
1725
+ _TDateTime_co = TypeVar(
1726
+ "_TDateTime_co", bound=ZonedDateTime | dt.datetime, covariant=True
1727
+ )
1728
+
1729
+
1730
+ @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
1731
+ class ZonedDateTimePeriod:
1732
+ """A period of time."""
1733
+
1734
+ start: ZonedDateTime
1735
+ end: ZonedDateTime
1736
+
1737
+ def __post_init__(self) -> None:
1738
+ if self.start > self.end:
1739
+ raise _ZonedDateTimePeriodInvalidError(start=self.start, end=self.end)
1740
+ if self.start.tz != self.end.tz:
1741
+ raise _ZonedDateTimePeriodTimeZoneError(
1742
+ start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
1743
+ )
1744
+
1745
+ def __add__(self, other: TimeDelta, /) -> Self:
1746
+ """Offset the period."""
1747
+ return self.replace(start=self.start + other, end=self.end + other)
1748
+
1749
+ def __contains__(self, other: ZonedDateTime, /) -> bool:
1750
+ """Check if a date/datetime lies in the period."""
1751
+ return self.start <= other <= self.end
1752
+
1753
+ @override
1754
+ def __repr__(self) -> str:
1755
+ cls = get_class_name(self)
1756
+ return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
1757
+
1758
+ def __sub__(self, other: TimeDelta, /) -> Self:
1759
+ """Offset the period."""
1760
+ return self.replace(start=self.start - other, end=self.end - other)
1761
+
1762
+ @property
1763
+ def delta(self) -> TimeDelta:
1764
+ """The duration of the period."""
1765
+ return self.end - self.start
1766
+
1767
+ @overload
1768
+ def exact_eq(self, period: ZonedDateTimePeriod, /) -> bool: ...
1769
+ @overload
1770
+ def exact_eq(self, start: ZonedDateTime, end: ZonedDateTime, /) -> bool: ...
1771
+ @overload
1772
+ def exact_eq(
1773
+ self, start: PlainDateTime, end: PlainDateTime, time_zone: ZoneInfo, /
1774
+ ) -> bool: ...
1775
+ def exact_eq(self, *args: Any) -> bool:
1776
+ """Check if a period is exactly equal to another."""
1777
+ if (len(args) == 1) and isinstance(args[0], ZonedDateTimePeriod):
1778
+ return self.start.exact_eq(args[0].start) and self.end.exact_eq(args[0].end)
1779
+ if (
1780
+ (len(args) == 2)
1781
+ and isinstance(args[0], ZonedDateTime)
1782
+ and isinstance(args[1], ZonedDateTime)
1783
+ ):
1784
+ return self.exact_eq(ZonedDateTimePeriod(args[0], args[1]))
1785
+ if (
1786
+ (len(args) == 3)
1787
+ and isinstance(args[0], PlainDateTime)
1788
+ and isinstance(args[1], PlainDateTime)
1789
+ and isinstance(args[2], ZoneInfo)
1790
+ ):
1791
+ return self.exact_eq(
1792
+ ZonedDateTimePeriod(
1793
+ args[0].assume_tz(args[2].key), args[1].assume_tz(args[2].key)
1794
+ )
1795
+ )
1796
+ raise _ZonedDateTimePeriodExactEqError(args=args)
1797
+
1798
+ def format_compact(self) -> str:
1799
+ """Format the period in a compact fashion."""
1800
+ fc, start, end = format_compact, self.start, self.end
1801
+ if start == end:
1802
+ if end.second != 0:
1803
+ return f"{fc(start)}="
1804
+ if end.minute != 0:
1805
+ return f"{fc(start, fmt='%Y%m%dT%H%M')}="
1806
+ return f"{fc(start, fmt='%Y%m%dT%H')}="
1807
+ if start.date() == end.date():
1808
+ if end.second != 0:
1809
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M%S')}"
1810
+ if end.minute != 0:
1811
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%H%M')}"
1812
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%H')}"
1813
+ if start.date().year_month() == end.date().year_month():
1814
+ if end.second != 0:
1815
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M%S')}"
1816
+ if end.minute != 0:
1817
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H%M')}"
1818
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%dT%H')}"
1819
+ if start.year == end.year:
1820
+ if end.second != 0:
1821
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M%S')}"
1822
+ if end.minute != 0:
1823
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H%M')}"
1824
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%m%dT%H')}"
1825
+ if end.second != 0:
1826
+ return f"{fc(start.to_plain())}-{fc(end)}"
1827
+ if end.minute != 0:
1828
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H%M')}"
1829
+ return f"{fc(start.to_plain())}-{fc(end, fmt='%Y%m%dT%H')}"
1830
+
1831
+ @classmethod
1832
+ def from_dict(cls, mapping: PeriodDict[_TDateTime_co], /) -> Self:
1833
+ """Convert the dictionary to a period."""
1834
+ match mapping["start"]:
1835
+ case ZonedDateTime() as start:
1836
+ ...
1837
+ case dt.date() as py_datetime:
1838
+ start = ZonedDateTime.from_py_datetime(py_datetime)
1839
+ case never:
1840
+ assert_never(never)
1841
+ match mapping["end"]:
1842
+ case ZonedDateTime() as end:
1843
+ ...
1844
+ case dt.date() as py_datetime:
1845
+ end = ZonedDateTime.from_py_datetime(py_datetime)
1846
+ case never:
1847
+ assert_never(never)
1848
+ return cls(start=start, end=end)
1849
+
1850
+ def replace(
1851
+ self,
1852
+ *,
1853
+ start: ZonedDateTime | Sentinel = sentinel,
1854
+ end: ZonedDateTime | Sentinel = sentinel,
1855
+ ) -> Self:
1856
+ """Replace elements of the period."""
1857
+ return replace_non_sentinel(self, start=start, end=end)
1858
+
1859
+ @property
1860
+ def time_zone(self) -> ZoneInfo:
1861
+ """The time zone of the period."""
1862
+ return ZoneInfo(self.start.tz)
1863
+
1864
+ def to_dict(self) -> PeriodDict[ZonedDateTime]:
1865
+ """Convert the period to a dictionary."""
1866
+ return PeriodDict(start=self.start, end=self.end)
1867
+
1868
+ def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
1869
+ """Convert the time zone."""
1870
+ tz = get_time_zone_name(time_zone)
1871
+ return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
1872
+
1873
+
1874
+ @dataclass(kw_only=True, slots=True)
1875
+ class ZonedDateTimePeriodError(Exception): ...
1876
+
1877
+
1878
+ @dataclass(kw_only=True, slots=True)
1879
+ class _ZonedDateTimePeriodInvalidError[T: Date | ZonedDateTime](
1880
+ ZonedDateTimePeriodError
1881
+ ):
1882
+ start: T
1883
+ end: T
1884
+
1885
+ @override
1886
+ def __str__(self) -> str:
1887
+ return f"Invalid period; got {self.start} > {self.end}"
1888
+
1889
+
1890
+ @dataclass(kw_only=True, slots=True)
1891
+ class _ZonedDateTimePeriodTimeZoneError(ZonedDateTimePeriodError):
1892
+ start: ZoneInfo
1893
+ end: ZoneInfo
1894
+
1895
+ @override
1896
+ def __str__(self) -> str:
1897
+ return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
1898
+
1899
+
1900
+ @dataclass(kw_only=True, slots=True)
1901
+ class _ZonedDateTimePeriodExactEqError(ZonedDateTimePeriodError):
1902
+ args: tuple[Any, ...]
1903
+
1904
+ @override
1905
+ def __str__(self) -> str:
1906
+ return f"Invalid arguments; got {self.args}"
1907
+
1908
+
1521
1909
  __all__ = [
1522
1910
  "DATE_DELTA_MAX",
1523
1911
  "DATE_DELTA_MIN",
@@ -1547,9 +1935,13 @@ __all__ = [
1547
1935
  "ZERO_TIME",
1548
1936
  "ZONED_DATE_TIME_MAX",
1549
1937
  "ZONED_DATE_TIME_MIN",
1938
+ "DatePeriod",
1939
+ "DatePeriodError",
1550
1940
  "MeanDateTimeError",
1551
1941
  "MinMaxDateError",
1942
+ "PeriodDict",
1552
1943
  "RoundDateOrDateTimeError",
1944
+ "TimePeriod",
1553
1945
  "ToDaysError",
1554
1946
  "ToMinutesError",
1555
1947
  "ToMonthsAndDaysError",
@@ -1560,6 +1952,8 @@ __all__ = [
1560
1952
  "ToWeeksError",
1561
1953
  "ToYearsError",
1562
1954
  "WheneverLogRecord",
1955
+ "ZonedDateTimePeriod",
1956
+ "ZonedDateTimePeriodError",
1563
1957
  "add_year_month",
1564
1958
  "datetime_utc",
1565
1959
  "diff_year_month",