dycw-utilities 0.152.0__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.
- {dycw_utilities-0.152.0.dist-info → dycw_utilities-0.153.0.dist-info}/METADATA +1 -1
- {dycw_utilities-0.152.0.dist-info → dycw_utilities-0.153.0.dist-info}/RECORD +26 -27
- utilities/__init__.py +1 -1
- utilities/asyncio.py +12 -11
- utilities/cryptography.py +3 -3
- utilities/eventkit.py +8 -8
- utilities/functions.py +1 -33
- utilities/logging.py +23 -24
- utilities/pathlib.py +34 -34
- utilities/postgres.py +12 -12
- utilities/pottery.py +5 -5
- utilities/pyinstrument.py +3 -3
- utilities/pytest.py +8 -8
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +1 -1
- utilities/random.py +8 -6
- utilities/text.py +37 -2
- utilities/traceback.py +15 -18
- utilities/typed_settings.py +3 -3
- utilities/types.py +24 -14
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +431 -37
- utilities/period.py +0 -370
- {dycw_utilities-0.152.0.dist-info → dycw_utilities-0.153.0.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.152.0.dist-info → dycw_utilities-0.153.0.dist-info}/entry_points.txt +0 -0
- {dycw_utilities-0.152.0.dist-info → dycw_utilities-0.153.0.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
45
|
-
|
49
|
+
MaybeCallableDateLike,
|
50
|
+
MaybeCallableZonedDateTimeLike,
|
46
51
|
TimeOrDateTimeDelta,
|
47
52
|
TimeZoneLike,
|
48
53
|
)
|
49
54
|
|
55
|
+
# type vars
|
50
56
|
|
51
|
-
|
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(
|
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(
|
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(
|
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(
|
375
|
+
def get_today(time_zone: TimeZoneLike = UTC, /) -> Date:
|
263
376
|
"""Get the current, timezone-aware local date."""
|
264
|
-
return get_now(time_zone
|
377
|
+
return get_now(time_zone).date()
|
265
378
|
|
266
379
|
|
267
|
-
TODAY_UTC = get_today(
|
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(
|
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
|
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(*,
|
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
|
-
|
655
|
-
|
834
|
+
date: MaybeCallableDateLike | None | dt.date = get_today,
|
835
|
+
/,
|
836
|
+
*,
|
837
|
+
time_zone: TimeZoneLike = UTC,
|
838
|
+
) -> Date: ...
|
656
839
|
def to_date(
|
657
|
-
|
658
|
-
|
659
|
-
|
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() |
|
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(
|
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
|
-
*,
|
1422
|
-
) ->
|
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
|
-
|
1429
|
-
|
1430
|
-
|
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() |
|
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(
|
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",
|