dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__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.
Files changed (103) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.17.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +14 -14
  7. utilities/asyncio.py +350 -819
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +77 -22
  10. utilities/cachetools.py +24 -29
  11. utilities/click.py +393 -237
  12. utilities/concurrent.py +8 -11
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +83 -118
  17. utilities/docker.py +293 -0
  18. utilities/enum.py +26 -23
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +29 -65
  21. utilities/fpdf2.py +3 -3
  22. utilities/functions.py +169 -416
  23. utilities/functools.py +18 -19
  24. utilities/git.py +9 -30
  25. utilities/grp.py +28 -0
  26. utilities/gzip.py +31 -0
  27. utilities/http.py +3 -2
  28. utilities/hypothesis.py +738 -589
  29. utilities/importlib.py +17 -1
  30. utilities/inflect.py +25 -0
  31. utilities/iterables.py +194 -262
  32. utilities/jinja2.py +148 -0
  33. utilities/json.py +70 -0
  34. utilities/libcst.py +38 -17
  35. utilities/lightweight_charts.py +5 -9
  36. utilities/logging.py +345 -543
  37. utilities/math.py +18 -13
  38. utilities/memory_profiler.py +11 -15
  39. utilities/more_itertools.py +200 -131
  40. utilities/operator.py +33 -29
  41. utilities/optuna.py +6 -6
  42. utilities/orjson.py +272 -137
  43. utilities/os.py +61 -4
  44. utilities/parse.py +59 -61
  45. utilities/pathlib.py +281 -40
  46. utilities/permissions.py +298 -0
  47. utilities/pickle.py +2 -2
  48. utilities/platform.py +24 -5
  49. utilities/polars.py +1214 -430
  50. utilities/polars_ols.py +1 -1
  51. utilities/postgres.py +408 -0
  52. utilities/pottery.py +113 -26
  53. utilities/pqdm.py +10 -11
  54. utilities/psutil.py +6 -57
  55. utilities/pwd.py +28 -0
  56. utilities/pydantic.py +4 -54
  57. utilities/pydantic_settings.py +240 -0
  58. utilities/pydantic_settings_sops.py +76 -0
  59. utilities/pyinstrument.py +8 -10
  60. utilities/pytest.py +227 -121
  61. utilities/pytest_plugins/__init__.py +1 -0
  62. utilities/pytest_plugins/pytest_randomly.py +23 -0
  63. utilities/pytest_plugins/pytest_regressions.py +56 -0
  64. utilities/pytest_regressions.py +26 -46
  65. utilities/random.py +13 -9
  66. utilities/re.py +58 -28
  67. utilities/redis.py +401 -550
  68. utilities/scipy.py +1 -1
  69. utilities/sentinel.py +10 -0
  70. utilities/shelve.py +4 -1
  71. utilities/shutil.py +25 -0
  72. utilities/slack_sdk.py +36 -106
  73. utilities/sqlalchemy.py +502 -473
  74. utilities/sqlalchemy_polars.py +38 -94
  75. utilities/string.py +2 -3
  76. utilities/subprocess.py +1572 -0
  77. utilities/tempfile.py +86 -4
  78. utilities/testbook.py +50 -0
  79. utilities/text.py +165 -42
  80. utilities/timer.py +37 -65
  81. utilities/traceback.py +158 -929
  82. utilities/types.py +146 -116
  83. utilities/typing.py +531 -71
  84. utilities/tzdata.py +1 -53
  85. utilities/tzlocal.py +6 -23
  86. utilities/uuid.py +43 -5
  87. utilities/version.py +27 -26
  88. utilities/whenever.py +1776 -386
  89. utilities/zoneinfo.py +84 -22
  90. dycw_utilities-0.129.10.dist-info/METADATA +0 -241
  91. dycw_utilities-0.129.10.dist-info/RECORD +0 -96
  92. dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
  93. dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
  94. utilities/datetime.py +0 -1409
  95. utilities/eventkit.py +0 -402
  96. utilities/loguru.py +0 -144
  97. utilities/luigi.py +0 -228
  98. utilities/period.py +0 -324
  99. utilities/pyrsistent.py +0 -89
  100. utilities/python_dotenv.py +0 -105
  101. utilities/streamlit.py +0 -105
  102. utilities/sys.py +0 -87
  103. utilities/tenacity.py +0 -145
utilities/whenever.py CHANGED
@@ -1,565 +1,1775 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
- import re
5
- from contextlib import suppress
4
+ from collections.abc import Callable, Iterable
6
5
  from dataclasses import dataclass
7
6
  from functools import cache
8
7
  from logging import LogRecord
9
- from typing import TYPE_CHECKING, Any, override
8
+ from statistics import fmean
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Literal,
13
+ Self,
14
+ SupportsFloat,
15
+ TypedDict,
16
+ assert_never,
17
+ cast,
18
+ overload,
19
+ override,
20
+ )
21
+ from zoneinfo import ZoneInfo
10
22
 
11
23
  from whenever import (
12
24
  Date,
25
+ DateDelta,
13
26
  DateTimeDelta,
14
27
  PlainDateTime,
15
28
  Time,
16
- TimeZoneNotFoundError,
29
+ TimeDelta,
30
+ Weekday,
31
+ YearMonth,
17
32
  ZonedDateTime,
18
33
  )
19
34
 
20
- from utilities.datetime import (
21
- _MICROSECONDS_PER_DAY,
22
- _MICROSECONDS_PER_SECOND,
23
- ZERO_TIME,
24
- check_date_not_datetime,
25
- datetime_duration_to_microseconds,
26
- parse_two_digit_year,
27
- )
28
- from utilities.math import ParseNumberError, parse_number
29
- from utilities.re import (
30
- ExtractGroupError,
31
- ExtractGroupsError,
32
- extract_group,
33
- extract_groups,
34
- )
35
- from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
35
+ from utilities.dataclasses import replace_non_sentinel
36
+ from utilities.functions import get_class_name
37
+ from utilities.math import sign
38
+ from utilities.platform import get_strftime
39
+ from utilities.sentinel import Sentinel, sentinel
40
+ from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
41
+ from utilities.zoneinfo import UTC, to_time_zone_name
36
42
 
37
43
  if TYPE_CHECKING:
38
- from zoneinfo import ZoneInfo
39
-
40
44
  from utilities.types import (
41
- DateLike,
42
- DateTimeLike,
43
- Duration,
44
- DurationLike,
45
- TimeDeltaLike,
46
- TimeLike,
45
+ DateOrDateTimeDelta,
46
+ DateTimeRoundMode,
47
+ Delta,
48
+ MaybeCallableDateLike,
49
+ MaybeCallableTimeLike,
50
+ MaybeCallableZonedDateTimeLike,
51
+ TimeOrDateTimeDelta,
52
+ TimeZoneLike,
47
53
  )
48
54
 
49
55
 
50
- MAX_SERIALIZABLE_TIMEDELTA = dt.timedelta(days=3652060, microseconds=-1)
51
- MIN_SERIALIZABLE_TIMEDELTA = -MAX_SERIALIZABLE_TIMEDELTA
56
+ # bounds
57
+
58
+
59
+ ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key)
60
+ ZONED_DATE_TIME_MAX = PlainDateTime.MAX.assume_tz(UTC.key)
61
+
62
+
63
+ DATE_TIME_DELTA_MIN = DateTimeDelta(
64
+ weeks=-521722,
65
+ days=-5,
66
+ hours=-23,
67
+ minutes=-59,
68
+ seconds=-59,
69
+ milliseconds=-999,
70
+ microseconds=-999,
71
+ nanoseconds=-999,
72
+ )
73
+ DATE_TIME_DELTA_MAX = DateTimeDelta(
74
+ weeks=521722,
75
+ days=5,
76
+ hours=23,
77
+ minutes=59,
78
+ seconds=59,
79
+ milliseconds=999,
80
+ microseconds=999,
81
+ nanoseconds=999,
82
+ )
83
+ DATE_DELTA_MIN = DATE_TIME_DELTA_MIN.date_part()
84
+ DATE_DELTA_MAX = DATE_TIME_DELTA_MAX.date_part()
85
+ TIME_DELTA_MIN = TimeDelta(hours=-87831216)
86
+ TIME_DELTA_MAX = TimeDelta(hours=87831216)
87
+
88
+
89
+ DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta(
90
+ weeks=-142857,
91
+ hours=-23,
92
+ minutes=-59,
93
+ seconds=-59,
94
+ milliseconds=-999,
95
+ microseconds=-999,
96
+ nanoseconds=-999,
97
+ )
98
+ DATE_TIME_DELTA_PARSABLE_MAX = DateTimeDelta(
99
+ weeks=142857,
100
+ hours=23,
101
+ minutes=59,
102
+ seconds=59,
103
+ milliseconds=999,
104
+ microseconds=999,
105
+ nanoseconds=999,
106
+ )
107
+ DATE_DELTA_PARSABLE_MIN = DateDelta(days=-999999)
108
+ DATE_DELTA_PARSABLE_MAX = DateDelta(days=999999)
109
+
110
+
111
+ DATE_TWO_DIGIT_YEAR_MIN = Date(1969, 1, 1)
112
+ DATE_TWO_DIGIT_YEAR_MAX = Date(DATE_TWO_DIGIT_YEAR_MIN.year + 99, 12, 31)
113
+
114
+
115
+ ## common constants
116
+
117
+
118
+ ZERO_DAYS = DateDelta()
119
+ ZERO_TIME = TimeDelta()
120
+ MICROSECOND = TimeDelta(microseconds=1)
121
+ MILLISECOND = TimeDelta(milliseconds=1)
122
+ SECOND = TimeDelta(seconds=1)
123
+ MINUTE = TimeDelta(minutes=1)
124
+ HOUR = TimeDelta(hours=1)
125
+ DAY = DateDelta(days=1)
126
+ WEEK = DateDelta(weeks=1)
127
+ MONTH = DateDelta(months=1)
128
+ YEAR = DateDelta(years=1)
52
129
 
53
130
 
54
131
  ##
55
132
 
56
133
 
57
- def check_valid_zoned_datetime(datetime: dt.datetime, /) -> None:
58
- """Check if a zoned datetime is valid."""
59
- time_zone = ensure_time_zone(datetime) # skipif-ci-and-windows
60
- datetime2 = datetime.replace(tzinfo=time_zone) # skipif-ci-and-windows
61
- try: # skipif-ci-and-windows
62
- result = (
63
- ZonedDateTime.from_py_datetime(datetime2)
64
- .to_tz(get_time_zone_name(UTC))
65
- .to_tz(get_time_zone_name(time_zone))
66
- .py_datetime()
67
- )
68
- except TimeZoneNotFoundError: # pragma: no cover
69
- raise _CheckValidZonedDateTimeInvalidTimeZoneError(datetime=datetime) from None
70
- if result != datetime2: # skipif-ci-and-windows
71
- raise _CheckValidZonedDateTimeUnequalError(datetime=datetime, result=result)
134
+ def add_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearMonth:
135
+ """Add to a year-month."""
136
+ y = x.on_day(1) + DateDelta(years=years, months=months)
137
+ return y.year_month()
72
138
 
73
139
 
74
- @dataclass(kw_only=True, slots=True)
75
- class CheckValidZonedDateTimeError(Exception):
76
- datetime: dt.datetime
140
+ def sub_year_month(x: YearMonth, /, *, years: int = 0, months: int = 0) -> YearMonth:
141
+ """Subtract from a year-month."""
142
+ y = x.on_day(1) - DateDelta(years=years, months=months)
143
+ return y.year_month()
77
144
 
78
145
 
79
- @dataclass(kw_only=True, slots=True)
80
- class _CheckValidZonedDateTimeInvalidTimeZoneError(CheckValidZonedDateTimeError):
146
+ ##
147
+
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
+
81
168
  @override
82
- def __str__(self) -> str:
83
- return f"Invalid timezone; got {self.datetime.tzinfo}" # pragma: no cover
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())
84
241
 
85
242
 
86
243
  @dataclass(kw_only=True, slots=True)
87
- class _CheckValidZonedDateTimeUnequalError(CheckValidZonedDateTimeError):
88
- result: dt.datetime
244
+ class DatePeriodError(Exception):
245
+ start: Date
246
+ end: Date
89
247
 
90
248
  @override
91
249
  def __str__(self) -> str:
92
- return f"Zoned datetime must be valid; got {self.datetime} != {self.result}" # skipif-ci-and-windows
250
+ return f"Invalid period; got {self.start} > {self.end}"
93
251
 
94
252
 
95
253
  ##
96
254
 
97
255
 
98
- def ensure_date(date: DateLike, /) -> dt.date:
99
- """Ensure the object is a date."""
100
- if isinstance(date, dt.date):
101
- check_date_not_datetime(date)
102
- return date
103
- try:
104
- return parse_date(date)
105
- except ParseDateError as error:
106
- raise EnsureDateError(date=error.date) from None
256
+ def datetime_utc(
257
+ year: int,
258
+ month: int,
259
+ day: int,
260
+ /,
261
+ hour: int = 0,
262
+ minute: int = 0,
263
+ second: int = 0,
264
+ millisecond: int = 0,
265
+ microsecond: int = 0,
266
+ nanosecond: int = 0,
267
+ ) -> ZonedDateTime:
268
+ """Create a UTC-zoned datetime."""
269
+ nanos = int(1e6) * millisecond + int(1e3) * microsecond + nanosecond
270
+ return ZonedDateTime(
271
+ year,
272
+ month,
273
+ day,
274
+ hour=hour,
275
+ minute=minute,
276
+ second=second,
277
+ nanosecond=nanos,
278
+ tz=UTC.key,
279
+ )
107
280
 
108
281
 
109
- @dataclass(kw_only=True, slots=True)
110
- class EnsureDateError(Exception):
111
- date: str
282
+ ##
283
+
284
+
285
+ @overload
286
+ def diff_year_month(
287
+ x: YearMonth, y: YearMonth, /, *, years: Literal[True]
288
+ ) -> tuple[int, int]: ...
289
+ @overload
290
+ def diff_year_month(
291
+ x: YearMonth, y: YearMonth, /, *, years: Literal[False] = False
292
+ ) -> int: ...
293
+ @overload
294
+ def diff_year_month(
295
+ x: YearMonth, y: YearMonth, /, *, years: bool = False
296
+ ) -> int | tuple[int, int]: ...
297
+ def diff_year_month(
298
+ x: YearMonth, y: YearMonth, /, *, years: bool = False
299
+ ) -> int | tuple[int, int]:
300
+ """Compute the difference between two year-months."""
301
+ x_date, y_date = x.on_day(1), y.on_day(1)
302
+ diff = x_date - y_date
303
+ if years:
304
+ yrs, mth, _ = diff.in_years_months_days()
305
+ return yrs, mth
306
+ mth, _ = diff.in_months_days()
307
+ return mth
308
+
309
+
310
+ ##
311
+
312
+
313
+ def format_compact(
314
+ obj: Date | Time | PlainDateTime | ZonedDateTime,
315
+ /,
316
+ *,
317
+ fmt: str | None = None,
318
+ path: bool = False,
319
+ ) -> str:
320
+ """Format the date/datetime in a compact fashion."""
321
+ match obj:
322
+ case Date() as date:
323
+ obj_use = date.py_date()
324
+ fmt_use = "%Y%m%d" if fmt is None else fmt
325
+ case Time() as time:
326
+ obj_use = time.round().py_time()
327
+ fmt_use = "%H%M%S" if fmt is None else fmt
328
+ case PlainDateTime() as date_time:
329
+ obj_use = date_time.round().py_datetime()
330
+ fmt_use = "%Y%m%dT%H%M%S" if fmt is None else fmt
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:
338
+ assert_never(never)
339
+ return obj_use.strftime(get_strftime(fmt_use))
340
+
341
+
342
+ ##
343
+
344
+
345
+ def from_timestamp(i: float, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
346
+ """Get a zoned datetime from a timestamp."""
347
+ return ZonedDateTime.from_timestamp(i, tz=to_time_zone_name(time_zone))
348
+
349
+
350
+ def from_timestamp_millis(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
351
+ """Get a zoned datetime from a timestamp (in milliseconds)."""
352
+ return ZonedDateTime.from_timestamp_millis(i, tz=to_time_zone_name(time_zone))
353
+
354
+
355
+ def from_timestamp_nanos(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
356
+ """Get a zoned datetime from a timestamp (in nanoseconds)."""
357
+ return ZonedDateTime.from_timestamp_nanos(i, tz=to_time_zone_name(time_zone))
358
+
359
+
360
+ ##
361
+
362
+
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))
366
+
367
+
368
+ NOW_UTC = get_now(UTC)
369
+
370
+
371
+ def get_now_local() -> ZonedDateTime:
372
+ """Get the current zoned date-time in the local time-zone."""
373
+ return get_now(LOCAL_TIME_ZONE)
374
+
375
+
376
+ NOW_LOCAL = get_now_local()
377
+
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
+
395
+ ##
396
+
397
+
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
+
112
413
 
414
+ ##
415
+
416
+
417
+ def get_today(time_zone: TimeZoneLike = UTC, /) -> Date:
418
+ """Get the current, timezone-aware local date."""
419
+ return get_now(time_zone).date()
420
+
421
+
422
+ TODAY_UTC = get_today(UTC)
423
+
424
+
425
+ def get_today_local() -> Date:
426
+ """Get the current, timezone-aware local date."""
427
+ return get_today(LOCAL_TIME_ZONE)
428
+
429
+
430
+ TODAY_LOCAL = get_today_local()
431
+
432
+
433
+ ##
434
+
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
+
466
+ def mean_datetime(
467
+ datetimes: Iterable[ZonedDateTime],
468
+ /,
469
+ *,
470
+ weights: Iterable[SupportsFloat] | None = None,
471
+ ) -> ZonedDateTime:
472
+ """Compute the mean of a set of datetimes."""
473
+ datetimes = list(datetimes)
474
+ match len(datetimes):
475
+ case 0:
476
+ raise MeanDateTimeError from None
477
+ case 1:
478
+ return datetimes[0]
479
+ case _:
480
+ timestamps = [d.timestamp_nanos() for d in datetimes]
481
+ timestamp = round(fmean(timestamps, weights=weights))
482
+ return ZonedDateTime.from_timestamp_nanos(timestamp, tz=datetimes[0].tz)
483
+
484
+
485
+ @dataclass(kw_only=True, slots=True)
486
+ class MeanDateTimeError(Exception):
113
487
  @override
114
488
  def __str__(self) -> str:
115
- return f"Unable to ensure date; got {self.date!r}"
489
+ return "Mean requires at least 1 datetime"
116
490
 
117
491
 
118
492
  ##
119
493
 
120
494
 
121
- def ensure_datetime(datetime: DateTimeLike, /) -> dt.datetime:
122
- """Ensure the object is a datetime."""
123
- if isinstance(datetime, dt.datetime):
124
- return datetime # skipif-ci-and-windows
125
- try:
126
- return parse_datetime(datetime)
127
- except ParseDateTimeError as error:
128
- raise EnsureDateTimeError(datetime=error.datetime) from None
495
+ def min_max_date(
496
+ *,
497
+ min_date: Date | None = None,
498
+ max_date: Date | None = None,
499
+ min_age: DateDelta | None = None,
500
+ max_age: DateDelta | None = None,
501
+ time_zone: TimeZoneLike = UTC,
502
+ ) -> tuple[Date | None, Date | None]:
503
+ """Compute the min/max date given a combination of dates/ages."""
504
+ today = get_today(time_zone)
505
+ min_parts: list[Date] = []
506
+ if min_date is not None:
507
+ min_parts.append(min_date)
508
+ if max_age is not None:
509
+ min_parts.append(today - max_age)
510
+ min_date_use = max(min_parts, default=None)
511
+ max_parts: list[Date] = []
512
+ if max_date is not None:
513
+ max_parts.append(max_date)
514
+ if min_age is not None:
515
+ max_parts.append(today - min_age)
516
+ max_date_use = min(max_parts, default=None)
517
+ if (
518
+ (min_date_use is not None)
519
+ and (max_date_use is not None)
520
+ and (min_date_use > max_date_use)
521
+ ):
522
+ raise _MinMaxDatePeriodError(min_date=min_date_use, max_date=max_date_use)
523
+ return min_date_use, max_date_use
129
524
 
130
525
 
131
526
  @dataclass(kw_only=True, slots=True)
132
- class EnsureDateTimeError(Exception):
133
- datetime: str
527
+ class MinMaxDateError(Exception):
528
+ min_date: Date
529
+ max_date: Date
134
530
 
531
+
532
+ @dataclass(kw_only=True, slots=True)
533
+ class _MinMaxDatePeriodError(MinMaxDateError):
135
534
  @override
136
535
  def __str__(self) -> str:
137
- return f"Unable to ensure datetime; got {self.datetime!r}"
536
+ return (
537
+ f"Min date must be at most max date; got {self.min_date} > {self.max_date}"
538
+ )
138
539
 
139
540
 
140
541
  ##
141
542
 
142
543
 
143
- def ensure_duration(duration: DurationLike, /) -> Duration:
144
- """Ensure the object is a Duration."""
145
- if isinstance(duration, int | float | dt.timedelta):
146
- return duration
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
+
556
+ type _RoundDateDailyUnit = Literal["W", "D"]
557
+ type _RoundDateTimeUnit = Literal["H", "M", "S", "ms", "us", "ns"]
558
+ type _RoundDateOrDateTimeUnit = _RoundDateDailyUnit | _RoundDateTimeUnit
559
+
560
+
561
+ def round_date_or_date_time[T: Date | PlainDateTime | ZonedDateTime](
562
+ date_or_date_time: T,
563
+ delta: Delta,
564
+ /,
565
+ *,
566
+ mode: DateTimeRoundMode = "half_even",
567
+ weekday: Weekday | None = None,
568
+ ) -> T:
569
+ """Round a datetime."""
570
+ increment, unit = _round_datetime_decompose(delta)
571
+ match date_or_date_time, unit, weekday:
572
+ case Date() as date, "W" | "D", _:
573
+ return _round_date_weekly_or_daily(
574
+ date, increment, unit, mode=mode, weekday=weekday
575
+ )
576
+ case Date() as date, "H" | "M" | "S" | "ms" | "us" | "ns", _:
577
+ raise _RoundDateOrDateTimeDateWithIntradayDeltaError(date=date, delta=delta)
578
+ case (PlainDateTime() | ZonedDateTime() as date_time, "W" | "D", _):
579
+ return _round_date_time_weekly_or_daily(
580
+ date_time, increment, unit, mode=mode, weekday=weekday
581
+ )
582
+ case (
583
+ PlainDateTime() | ZonedDateTime() as date_time,
584
+ "H" | "M" | "S" | "ms" | "us" | "ns",
585
+ None,
586
+ ):
587
+ return _round_date_time_intraday(date_time, increment, unit, mode=mode)
588
+ case (
589
+ PlainDateTime() | ZonedDateTime() as date_time,
590
+ "H" | "M" | "S" | "ms" | "us" | "ns",
591
+ Weekday(),
592
+ ):
593
+ raise _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(
594
+ date_time=date_time, delta=delta, weekday=weekday
595
+ )
596
+ case never:
597
+ assert_never(never)
598
+
599
+
600
+ def _round_datetime_decompose(delta: Delta, /) -> tuple[int, _RoundDateOrDateTimeUnit]:
601
+ try:
602
+ weeks = to_weeks(delta)
603
+ except ToWeeksError:
604
+ pass
605
+ else:
606
+ return weeks, "W"
607
+ try:
608
+ days = to_days(delta)
609
+ except ToDaysError:
610
+ pass
611
+ else:
612
+ return days, "D"
147
613
  try:
148
- return parse_duration(duration)
149
- except ParseDurationError as error:
150
- raise EnsureDurationError(duration=error.duration) from None
614
+ hours = to_hours(delta)
615
+ except ToHoursError:
616
+ pass
617
+ else:
618
+ if (0 < hours < 24) and (24 % hours == 0):
619
+ return hours, "H"
620
+ raise _RoundDateOrDateTimeIncrementError(
621
+ duration=delta, increment=hours, divisor=24
622
+ )
623
+ try:
624
+ minutes = to_minutes(delta)
625
+ except ToMinutesError:
626
+ pass
627
+ else:
628
+ if (0 < minutes < 60) and (60 % minutes == 0):
629
+ return minutes, "M"
630
+ raise _RoundDateOrDateTimeIncrementError(
631
+ duration=delta, increment=minutes, divisor=60
632
+ )
633
+ try:
634
+ seconds = to_seconds(delta)
635
+ except ToSecondsError:
636
+ pass
637
+ else:
638
+ if (0 < seconds < 60) and (60 % seconds == 0):
639
+ return seconds, "S"
640
+ raise _RoundDateOrDateTimeIncrementError(
641
+ duration=delta, increment=seconds, divisor=60
642
+ )
643
+ try:
644
+ milliseconds = to_milliseconds(delta)
645
+ except ToMillisecondsError:
646
+ pass
647
+ else:
648
+ if (0 < milliseconds < 1000) and (1000 % milliseconds == 0):
649
+ return milliseconds, "ms"
650
+ raise _RoundDateOrDateTimeIncrementError(
651
+ duration=delta, increment=milliseconds, divisor=1000
652
+ )
653
+ try:
654
+ microseconds = to_microseconds(delta)
655
+ except ToMicrosecondsError:
656
+ pass
657
+ else:
658
+ if (0 < microseconds < 1000) and (1000 % microseconds == 0):
659
+ return microseconds, "us"
660
+ raise _RoundDateOrDateTimeIncrementError(
661
+ duration=delta, increment=microseconds, divisor=1000
662
+ )
663
+ try:
664
+ nanoseconds = to_nanoseconds(delta)
665
+ except ToNanosecondsError:
666
+ raise _RoundDateOrDateTimeInvalidDurationError(duration=delta) from None
667
+ if (0 < nanoseconds < 1000) and (1000 % nanoseconds == 0):
668
+ return nanoseconds, "ns"
669
+ raise _RoundDateOrDateTimeIncrementError(
670
+ duration=delta, increment=nanoseconds, divisor=1000
671
+ )
672
+
673
+
674
+ def _round_date_weekly_or_daily(
675
+ date: Date,
676
+ increment: int,
677
+ unit: _RoundDateDailyUnit,
678
+ /,
679
+ *,
680
+ mode: DateTimeRoundMode = "half_even",
681
+ weekday: Weekday | None = None,
682
+ ) -> Date:
683
+ match unit, weekday:
684
+ case "W", _:
685
+ return _round_date_weekly(date, increment, mode=mode, weekday=weekday)
686
+ case "D", None:
687
+ return _round_date_daily(date, increment, mode=mode)
688
+ case "D", Weekday():
689
+ raise _RoundDateOrDateTimeDateWithWeekdayError(weekday=weekday)
690
+ case never:
691
+ assert_never(never)
692
+
693
+
694
+ def _round_date_weekly(
695
+ date: Date,
696
+ increment: int,
697
+ /,
698
+ *,
699
+ mode: DateTimeRoundMode = "half_even",
700
+ weekday: Weekday | None = None,
701
+ ) -> Date:
702
+ mapping = {
703
+ None: 0,
704
+ Weekday.MONDAY: 0,
705
+ Weekday.TUESDAY: 1,
706
+ Weekday.WEDNESDAY: 2,
707
+ Weekday.THURSDAY: 3,
708
+ Weekday.FRIDAY: 4,
709
+ Weekday.SATURDAY: 5,
710
+ Weekday.SUNDAY: 6,
711
+ }
712
+ base = Date.MIN.add(days=mapping[weekday])
713
+ return _round_date_daily(date, 7 * increment, mode=mode, base=base)
714
+
715
+
716
+ def _round_date_daily(
717
+ date: Date,
718
+ increment: int,
719
+ /,
720
+ *,
721
+ mode: DateTimeRoundMode = "half_even",
722
+ base: Date = Date.MIN,
723
+ ) -> Date:
724
+ quotient, remainder = divmod(date.days_since(base), increment)
725
+ match mode:
726
+ case "half_even":
727
+ threshold = increment // 2 + (quotient % 2 == 0) or 1
728
+ case "ceil":
729
+ threshold = 1
730
+ case "floor":
731
+ threshold = increment + 1
732
+ case "half_floor":
733
+ threshold = increment // 2 + 1
734
+ case "half_ceil":
735
+ threshold = increment // 2 or 1
736
+ case never:
737
+ assert_never(never)
738
+ round_up = remainder >= threshold
739
+ return base.add(days=(quotient + round_up) * increment)
740
+
741
+
742
+ def _round_date_time_intraday[T: PlainDateTime | ZonedDateTime](
743
+ date_time: T,
744
+ increment: int,
745
+ unit: _RoundDateTimeUnit,
746
+ /,
747
+ *,
748
+ mode: DateTimeRoundMode = "half_even",
749
+ ) -> T:
750
+ match unit:
751
+ case "H":
752
+ unit_use = "hour"
753
+ case "M":
754
+ unit_use = "minute"
755
+ case "S":
756
+ unit_use = "second"
757
+ case "ms":
758
+ unit_use = "millisecond"
759
+ case "us":
760
+ unit_use = "microsecond"
761
+ case "ns":
762
+ unit_use = "nanosecond"
763
+ case never:
764
+ assert_never(never)
765
+ return date_time.round(unit_use, increment=increment, mode=mode)
766
+
767
+
768
+ def _round_date_time_weekly_or_daily[T: PlainDateTime | ZonedDateTime](
769
+ date_time: T,
770
+ increment: int,
771
+ unit: _RoundDateDailyUnit,
772
+ /,
773
+ *,
774
+ mode: DateTimeRoundMode = "half_even",
775
+ weekday: Weekday | None = None,
776
+ ) -> T:
777
+ rounded = cast("T", date_time.round("day", mode=mode))
778
+ new_date = _round_date_weekly_or_daily(
779
+ rounded.date(), increment, unit, mode=mode, weekday=weekday
780
+ )
781
+ return date_time.replace_date(new_date).replace_time(Time())
782
+
783
+
784
+ @dataclass(kw_only=True, slots=True)
785
+ class RoundDateOrDateTimeError(Exception): ...
151
786
 
152
787
 
153
788
  @dataclass(kw_only=True, slots=True)
154
- class EnsureDurationError(Exception):
155
- duration: str
789
+ class _RoundDateOrDateTimeIncrementError(RoundDateOrDateTimeError):
790
+ duration: Delta
791
+ increment: int
792
+ divisor: int
156
793
 
157
794
  @override
158
795
  def __str__(self) -> str:
159
- return f"Unable to ensure duration; got {self.duration!r}"
796
+ return f"Duration {self.duration} increment must be a proper divisor of {self.divisor}; got {self.increment}"
160
797
 
161
798
 
162
- ##
799
+ @dataclass(kw_only=True, slots=True)
800
+ class _RoundDateOrDateTimeInvalidDurationError(RoundDateOrDateTimeError):
801
+ duration: Delta
163
802
 
803
+ @override
804
+ def __str__(self) -> str:
805
+ return f"Duration must be valid; got {self.duration}"
164
806
 
165
- def ensure_plain_datetime(datetime: DateTimeLike, /) -> dt.datetime:
166
- """Ensure the object is a plain datetime."""
167
- if isinstance(datetime, dt.datetime):
168
- return datetime
169
- try:
170
- return parse_plain_datetime(datetime)
171
- except ParsePlainDateTimeError as error:
172
- raise EnsurePlainDateTimeError(datetime=error.datetime) from None
807
+
808
+ @dataclass(kw_only=True, slots=True)
809
+ class _RoundDateOrDateTimeDateWithIntradayDeltaError(RoundDateOrDateTimeError):
810
+ date: Date
811
+ delta: Delta
812
+
813
+ @override
814
+ def __str__(self) -> str:
815
+ return f"Dates must not be given intraday durations; got {self.date} and {self.delta}"
173
816
 
174
817
 
175
818
  @dataclass(kw_only=True, slots=True)
176
- class EnsurePlainDateTimeError(Exception):
177
- datetime: str
819
+ class _RoundDateOrDateTimeDateWithWeekdayError(RoundDateOrDateTimeError):
820
+ weekday: Weekday
178
821
 
179
822
  @override
180
823
  def __str__(self) -> str:
181
- return f"Unable to ensure plain datetime; got {self.datetime!r}"
824
+ return f"Daily rounding must not be given a weekday; got {self.weekday}"
825
+
826
+
827
+ @dataclass(kw_only=True, slots=True)
828
+ class _RoundDateOrDateTimeDateTimeIntraDayWithWeekdayError(RoundDateOrDateTimeError):
829
+ date_time: PlainDateTime | ZonedDateTime
830
+ delta: Delta
831
+ weekday: Weekday
832
+
833
+ @override
834
+ def __str__(self) -> str:
835
+ return f"Date-times and intraday rounding must not be given a weekday; got {self.date_time}, {self.delta} and {self.weekday}"
182
836
 
183
837
 
184
838
  ##
185
839
 
186
840
 
187
- def ensure_time(time: TimeLike, /) -> dt.time:
188
- """Ensure the object is a time."""
189
- if isinstance(time, dt.time):
190
- return time
191
- try:
192
- return parse_time(time)
193
- except ParseTimeError as error:
194
- raise EnsureTimeError(time=error.time) from None
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
+
903
+ @overload
904
+ def to_date(date: Sentinel, /, *, time_zone: TimeZoneLike = UTC) -> Sentinel: ...
905
+ @overload
906
+ def to_date(
907
+ date: MaybeCallableDateLike | None | dt.date = get_today,
908
+ /,
909
+ *,
910
+ time_zone: TimeZoneLike = UTC,
911
+ ) -> Date: ...
912
+ def to_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."""
919
+ match date:
920
+ case Date() | Sentinel():
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)
928
+ case Callable() as func:
929
+ return to_date(func(), time_zone=time_zone)
930
+ case never:
931
+ assert_never(never)
932
+
933
+
934
+ ##
935
+
936
+
937
+ def to_date_time_delta(nanos: int, /) -> DateTimeDelta:
938
+ """Construct a date-time delta."""
939
+ components = _to_time_delta_components(nanos)
940
+ days, hours = divmod(components.hours, 24)
941
+ weeks, days = divmod(days, 7)
942
+ match sign(nanos): # pragma: no cover
943
+ case 1:
944
+ if hours < 0:
945
+ hours += 24
946
+ days -= 1
947
+ if days < 0:
948
+ days += 7
949
+ weeks -= 1
950
+ case -1:
951
+ if hours > 0:
952
+ hours -= 24
953
+ days += 1
954
+ if days > 0:
955
+ days -= 7
956
+ weeks += 1
957
+ case 0:
958
+ ...
959
+ return DateTimeDelta(
960
+ weeks=weeks,
961
+ days=days,
962
+ hours=hours,
963
+ minutes=components.minutes,
964
+ seconds=components.seconds,
965
+ microseconds=components.microseconds,
966
+ milliseconds=components.milliseconds,
967
+ nanoseconds=components.nanoseconds,
968
+ )
969
+
970
+
971
+ ##
972
+
973
+
974
+ def to_days(delta: Delta, /) -> int:
975
+ """Compute the number of days in a delta."""
976
+ match delta:
977
+ case DateDelta():
978
+ months, days = delta.in_months_days()
979
+ if months != 0:
980
+ raise _ToDaysMonthsError(delta=delta, months=months)
981
+ return days
982
+ case TimeDelta():
983
+ nanos = to_nanoseconds(delta)
984
+ days, remainder = divmod(nanos, 24 * 60 * 60 * int(1e9))
985
+ if remainder != 0:
986
+ raise _ToDaysNanosecondsError(delta=delta, nanoseconds=remainder)
987
+ return days
988
+ case DateTimeDelta():
989
+ try:
990
+ return to_days(delta.date_part()) + to_days(delta.time_part())
991
+ except _ToDaysMonthsError as error:
992
+ raise _ToDaysMonthsError(delta=delta, months=error.months) from None
993
+ except _ToDaysNanosecondsError as error:
994
+ raise _ToDaysNanosecondsError(
995
+ delta=delta, nanoseconds=error.nanoseconds
996
+ ) from None
997
+ case never:
998
+ assert_never(never)
195
999
 
196
1000
 
197
1001
  @dataclass(kw_only=True, slots=True)
198
- class EnsureTimeError(Exception):
199
- time: str
1002
+ class ToDaysError(Exception): ...
1003
+
1004
+
1005
+ @dataclass(kw_only=True, slots=True)
1006
+ class _ToDaysMonthsError(ToDaysError):
1007
+ delta: DateOrDateTimeDelta
1008
+ months: int
200
1009
 
201
1010
  @override
202
1011
  def __str__(self) -> str:
203
- return f"Unable to ensure time; got {self.time!r}"
1012
+ return f"Delta must not contain months; got {self.months}"
1013
+
1014
+
1015
+ @dataclass(kw_only=True, slots=True)
1016
+ class _ToDaysNanosecondsError(ToDaysError):
1017
+ delta: TimeOrDateTimeDelta
1018
+ nanoseconds: int
1019
+
1020
+ @override
1021
+ def __str__(self) -> str:
1022
+ return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
204
1023
 
205
1024
 
206
1025
  ##
207
1026
 
208
1027
 
209
- def ensure_timedelta(timedelta: TimeDeltaLike, /) -> dt.timedelta:
210
- """Ensure the object is a timedelta."""
211
- if isinstance(timedelta, dt.timedelta):
212
- return timedelta
213
- try:
214
- return parse_timedelta(timedelta)
215
- except _ParseTimedeltaParseError as error:
216
- raise _EnsureTimedeltaParseError(timedelta=error.timedelta) from None
217
- except _ParseTimedeltaNanosecondError as error:
218
- raise _EnsureTimedeltaNanosecondError(
219
- timedelta=error.timedelta, nanoseconds=error.nanoseconds
220
- ) from None
1028
+ def to_hours(delta: Delta, /) -> int:
1029
+ """Compute the number of hours in a delta."""
1030
+ match delta:
1031
+ case DateDelta():
1032
+ try:
1033
+ days = to_days(delta)
1034
+ except _ToDaysMonthsError as error:
1035
+ raise _ToHoursMonthsError(delta=delta, months=error.months) from None
1036
+ return 24 * days
1037
+ case TimeDelta():
1038
+ nanos = to_nanoseconds(delta)
1039
+ divisor = 60 * 60 * int(1e9)
1040
+ hours, remainder = divmod(nanos, divisor)
1041
+ if remainder != 0:
1042
+ raise _ToHoursNanosecondsError(delta=delta, nanoseconds=remainder)
1043
+ return hours
1044
+ case DateTimeDelta():
1045
+ try:
1046
+ return to_hours(delta.date_part()) + to_hours(delta.time_part())
1047
+ except _ToHoursMonthsError as error:
1048
+ raise _ToHoursMonthsError(delta=delta, months=error.months) from None
1049
+ except _ToHoursNanosecondsError as error:
1050
+ raise _ToHoursNanosecondsError(
1051
+ delta=delta, nanoseconds=error.nanoseconds
1052
+ ) from None
1053
+ case never:
1054
+ assert_never(never)
221
1055
 
222
1056
 
223
1057
  @dataclass(kw_only=True, slots=True)
224
- class EnsureTimedeltaError(Exception):
225
- timedelta: str
1058
+ class ToHoursError(Exception): ...
226
1059
 
227
1060
 
228
1061
  @dataclass(kw_only=True, slots=True)
229
- class _EnsureTimedeltaParseError(EnsureTimedeltaError):
1062
+ class _ToHoursMonthsError(ToHoursError):
1063
+ delta: DateOrDateTimeDelta
1064
+ months: int
1065
+
230
1066
  @override
231
1067
  def __str__(self) -> str:
232
- return f"Unable to ensure timedelta; got {self.timedelta!r}"
1068
+ return f"Delta must not contain months; got {self.months}"
233
1069
 
234
1070
 
235
1071
  @dataclass(kw_only=True, slots=True)
236
- class _EnsureTimedeltaNanosecondError(EnsureTimedeltaError):
1072
+ class _ToHoursNanosecondsError(ToHoursError):
1073
+ delta: TimeOrDateTimeDelta
237
1074
  nanoseconds: int
238
1075
 
239
1076
  @override
240
1077
  def __str__(self) -> str:
241
- return f"Unable to ensure timedelta; got {self.nanoseconds} nanoseconds"
1078
+ return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
242
1079
 
243
1080
 
244
1081
  ##
245
1082
 
246
1083
 
247
- def ensure_zoned_datetime(datetime: DateTimeLike, /) -> dt.datetime:
248
- """Ensure the object is a zoned datetime."""
249
- if isinstance(datetime, dt.datetime):
250
- return datetime
251
- try:
252
- return parse_zoned_datetime(datetime)
253
- except ParseZonedDateTimeError as error:
254
- raise EnsureZonedDateTimeError(datetime=error.datetime) from None
1084
+ def to_microseconds(delta: Delta, /) -> int:
1085
+ """Compute the number of microseconds in a delta."""
1086
+ match delta:
1087
+ case DateDelta():
1088
+ try:
1089
+ days = to_days(delta)
1090
+ except _ToDaysMonthsError as error:
1091
+ raise _ToMicrosecondsMonthsError(
1092
+ delta=delta, months=error.months
1093
+ ) from None
1094
+ return 24 * 60 * 60 * int(1e6) * days
1095
+ case TimeDelta():
1096
+ nanos = to_nanoseconds(delta)
1097
+ microseconds, remainder = divmod(nanos, int(1e3))
1098
+ if remainder != 0:
1099
+ raise _ToMicrosecondsNanosecondsError(
1100
+ delta=delta, nanoseconds=remainder
1101
+ )
1102
+ return microseconds
1103
+ case DateTimeDelta():
1104
+ try:
1105
+ return to_microseconds(delta.date_part()) + to_microseconds(
1106
+ delta.time_part()
1107
+ )
1108
+ except _ToMicrosecondsMonthsError as error:
1109
+ raise _ToMicrosecondsMonthsError(
1110
+ delta=delta, months=error.months
1111
+ ) from None
1112
+ except _ToMicrosecondsNanosecondsError as error:
1113
+ raise _ToMicrosecondsNanosecondsError(
1114
+ delta=delta, nanoseconds=error.nanoseconds
1115
+ ) from None
1116
+ case never:
1117
+ assert_never(never)
255
1118
 
256
1119
 
257
1120
  @dataclass(kw_only=True, slots=True)
258
- class EnsureZonedDateTimeError(Exception):
259
- datetime: str
1121
+ class ToMicrosecondsError(Exception): ...
1122
+
1123
+
1124
+ @dataclass(kw_only=True, slots=True)
1125
+ class _ToMicrosecondsMonthsError(ToMicrosecondsError):
1126
+ delta: DateOrDateTimeDelta
1127
+ months: int
1128
+
1129
+ @override
1130
+ def __str__(self) -> str:
1131
+ return f"Delta must not contain months; got {self.months}"
1132
+
1133
+
1134
+ @dataclass(kw_only=True, slots=True)
1135
+ class _ToMicrosecondsNanosecondsError(ToMicrosecondsError):
1136
+ delta: TimeOrDateTimeDelta
1137
+ nanoseconds: int
260
1138
 
261
1139
  @override
262
1140
  def __str__(self) -> str:
263
- return f"Unable to ensure zoned datetime; got {self.datetime!r}"
1141
+ return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
264
1142
 
265
1143
 
266
1144
  ##
267
1145
 
268
1146
 
269
- _PARSE_DATE_YYMMDD_REGEX = re.compile(r"^(\d{2})(\d{2})(\d{2})$")
1147
+ def to_milliseconds(delta: Delta, /) -> int:
1148
+ """Compute the number of milliseconds in a delta."""
1149
+ match delta:
1150
+ case DateDelta():
1151
+ try:
1152
+ days = to_days(delta)
1153
+ except _ToDaysMonthsError as error:
1154
+ raise _ToMillisecondsMonthsError(
1155
+ delta=delta, months=error.months
1156
+ ) from None
1157
+ return 24 * 60 * 60 * int(1e3) * days
1158
+ case TimeDelta():
1159
+ nanos = to_nanoseconds(delta)
1160
+ milliseconds, remainder = divmod(nanos, int(1e6))
1161
+ if remainder != 0:
1162
+ raise _ToMillisecondsNanosecondsError(
1163
+ delta=delta, nanoseconds=remainder
1164
+ )
1165
+ return milliseconds
1166
+ case DateTimeDelta():
1167
+ try:
1168
+ return to_milliseconds(delta.date_part()) + to_milliseconds(
1169
+ delta.time_part()
1170
+ )
1171
+ except _ToMillisecondsMonthsError as error:
1172
+ raise _ToMillisecondsMonthsError(
1173
+ delta=delta, months=error.months
1174
+ ) from None
1175
+ except _ToMillisecondsNanosecondsError as error:
1176
+ raise _ToMillisecondsNanosecondsError(
1177
+ delta=delta, nanoseconds=error.nanoseconds
1178
+ ) from None
1179
+ case never:
1180
+ assert_never(never)
270
1181
 
271
1182
 
272
- def parse_date(date: str, /) -> dt.date:
273
- """Parse a string into a date."""
274
- try:
275
- w_date = Date.parse_common_iso(date)
276
- except ValueError:
277
- try:
278
- ((year2, month, day),) = _PARSE_DATE_YYMMDD_REGEX.findall(date)
279
- except ValueError:
280
- raise ParseDateError(date=date) from None
281
- year = parse_two_digit_year(year2)
282
- return dt.date(year=int(year), month=int(month), day=int(day))
283
- return w_date.py_date()
1183
+ @dataclass(kw_only=True, slots=True)
1184
+ class ToMillisecondsError(Exception): ...
284
1185
 
285
1186
 
286
1187
  @dataclass(kw_only=True, slots=True)
287
- class ParseDateError(Exception):
288
- date: str
1188
+ class _ToMillisecondsMonthsError(ToMillisecondsError):
1189
+ delta: DateOrDateTimeDelta
1190
+ months: int
289
1191
 
290
1192
  @override
291
1193
  def __str__(self) -> str:
292
- return f"Unable to parse date; got {self.date!r}"
1194
+ return f"Delta must not contain months; got {self.months}"
1195
+
1196
+
1197
+ @dataclass(kw_only=True, slots=True)
1198
+ class _ToMillisecondsNanosecondsError(ToMillisecondsError):
1199
+ delta: TimeOrDateTimeDelta
1200
+ nanoseconds: int
1201
+
1202
+ @override
1203
+ def __str__(self) -> str:
1204
+ return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
293
1205
 
294
1206
 
295
1207
  ##
296
1208
 
297
1209
 
298
- def parse_datetime(datetime: str, /) -> dt.datetime:
299
- """Parse a string into a datetime."""
300
- with suppress(ParsePlainDateTimeError):
301
- return parse_plain_datetime(datetime)
302
- with suppress(ParseZonedDateTimeError):
303
- return parse_zoned_datetime(datetime)
304
- raise ParseDateTimeError(datetime=datetime) from None
1210
+ def to_minutes(delta: Delta, /) -> int:
1211
+ """Compute the number of minutes in a delta."""
1212
+ match delta:
1213
+ case DateDelta():
1214
+ try:
1215
+ days = to_days(delta)
1216
+ except _ToDaysMonthsError as error:
1217
+ raise _ToMinutesMonthsError(delta=delta, months=error.months) from None
1218
+ return 24 * 60 * days
1219
+ case TimeDelta():
1220
+ nanos = to_nanoseconds(delta)
1221
+ minutes, remainder = divmod(nanos, 60 * int(1e9))
1222
+ if remainder != 0:
1223
+ raise _ToMinutesNanosecondsError(delta=delta, nanoseconds=remainder)
1224
+ return minutes
1225
+ case DateTimeDelta():
1226
+ try:
1227
+ return to_minutes(delta.date_part()) + to_minutes(delta.time_part())
1228
+ except _ToMinutesMonthsError as error:
1229
+ raise _ToMinutesMonthsError(delta=delta, months=error.months) from None
1230
+ except _ToMinutesNanosecondsError as error:
1231
+ raise _ToMinutesNanosecondsError(
1232
+ delta=delta, nanoseconds=error.nanoseconds
1233
+ ) from None
1234
+ case never:
1235
+ assert_never(never)
1236
+
1237
+
1238
+ @dataclass(kw_only=True, slots=True)
1239
+ class ToMinutesError(Exception): ...
305
1240
 
306
1241
 
307
1242
  @dataclass(kw_only=True, slots=True)
308
- class ParseDateTimeError(Exception):
309
- datetime: str
1243
+ class _ToMinutesMonthsError(ToMinutesError):
1244
+ delta: DateOrDateTimeDelta
1245
+ months: int
310
1246
 
311
1247
  @override
312
1248
  def __str__(self) -> str:
313
- return f"Unable to parse datetime; got {self.datetime!r}"
1249
+ return f"Delta must not contain months; got {self.months}"
1250
+
1251
+
1252
+ @dataclass(kw_only=True, slots=True)
1253
+ class _ToMinutesNanosecondsError(ToMinutesError):
1254
+ delta: TimeOrDateTimeDelta
1255
+ nanoseconds: int
1256
+
1257
+ @override
1258
+ def __str__(self) -> str:
1259
+ return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
314
1260
 
315
1261
 
316
1262
  ##
317
1263
 
318
1264
 
319
- def parse_duration(duration: str, /) -> Duration:
320
- """Parse a string into a Duration."""
321
- with suppress(ParseNumberError):
322
- return parse_number(duration)
323
- try:
324
- return parse_timedelta(duration)
325
- except ParseTimedeltaError:
326
- raise ParseDurationError(duration=duration) from None
1265
+ def to_months(delta: DateOrDateTimeDelta, /) -> int:
1266
+ """Compute the number of months in a delta."""
1267
+ match delta:
1268
+ case DateDelta():
1269
+ months, days = delta.in_months_days()
1270
+ if days != 0:
1271
+ raise _ToMonthsDaysError(delta=delta, days=days)
1272
+ return months
1273
+ case DateTimeDelta():
1274
+ if delta.time_part() != TimeDelta():
1275
+ raise _ToMonthsTimeError(delta=delta)
1276
+ try:
1277
+ return to_months(delta.date_part())
1278
+ except _ToMonthsDaysError as error:
1279
+ raise _ToMonthsDaysError(delta=delta, days=error.days) from None
1280
+ case never:
1281
+ assert_never(never)
1282
+
1283
+
1284
+ @dataclass(kw_only=True, slots=True)
1285
+ class ToMonthsError(Exception): ...
327
1286
 
328
1287
 
329
1288
  @dataclass(kw_only=True, slots=True)
330
- class ParseDurationError(Exception):
331
- duration: str
1289
+ class _ToMonthsDaysError(ToMonthsError):
1290
+ delta: DateOrDateTimeDelta
1291
+ days: int
332
1292
 
333
1293
  @override
334
1294
  def __str__(self) -> str:
335
- return f"Unable to parse duration; got {self.duration!r}"
1295
+ return f"Delta must not contain days; got {self.days}"
1296
+
1297
+
1298
+ @dataclass(kw_only=True, slots=True)
1299
+ class _ToMonthsTimeError(ToMonthsError):
1300
+ delta: DateTimeDelta
1301
+
1302
+ @override
1303
+ def __str__(self) -> str:
1304
+ return f"Delta must not contain a time part; got {self.delta.time_part()}"
336
1305
 
337
1306
 
338
1307
  ##
339
1308
 
340
1309
 
341
- def parse_plain_datetime(datetime: str, /) -> dt.datetime:
342
- """Parse a string into a plain datetime."""
343
- try:
344
- ldt = PlainDateTime.parse_common_iso(datetime)
345
- except ValueError:
346
- raise ParsePlainDateTimeError(datetime=datetime) from None
347
- return ldt.py_datetime()
1310
+ def to_months_and_days(delta: DateOrDateTimeDelta, /) -> tuple[int, int]:
1311
+ """Compute the number of months & days in a delta."""
1312
+ match delta:
1313
+ case DateDelta():
1314
+ return delta.in_months_days()
1315
+ case DateTimeDelta():
1316
+ if delta.time_part() != TimeDelta():
1317
+ raise ToMonthsAndDaysError(delta=delta)
1318
+ return to_months_and_days(delta.date_part())
1319
+ case never:
1320
+ assert_never(never)
348
1321
 
349
1322
 
350
1323
  @dataclass(kw_only=True, slots=True)
351
- class ParsePlainDateTimeError(Exception):
352
- datetime: str
1324
+ class ToMonthsAndDaysError(Exception):
1325
+ delta: DateTimeDelta
353
1326
 
354
1327
  @override
355
1328
  def __str__(self) -> str:
356
- return f"Unable to parse plain datetime; got {self.datetime!r}"
1329
+ return f"Delta must not contain a time part; got {self.delta.time_part()}"
357
1330
 
358
1331
 
359
1332
  ##
360
1333
 
361
1334
 
362
- def parse_time(time: str, /) -> dt.time:
363
- """Parse a string into a time."""
364
- try:
365
- w_time = Time.parse_common_iso(time)
366
- except ValueError:
367
- raise ParseTimeError(time=time) from None
368
- return w_time.py_time()
1335
+ def to_nanoseconds(delta: Delta, /) -> int:
1336
+ """Compute the number of nanoseconds in a date-time delta."""
1337
+ match delta:
1338
+ case DateDelta():
1339
+ try:
1340
+ days = to_days(delta)
1341
+ except _ToDaysMonthsError as error:
1342
+ raise ToNanosecondsError(delta=delta, months=error.months) from None
1343
+ return 24 * 60 * 60 * int(1e9) * days
1344
+ case TimeDelta():
1345
+ return delta.in_nanoseconds()
1346
+ case DateTimeDelta():
1347
+ try:
1348
+ return to_nanoseconds(delta.date_part()) + to_nanoseconds(
1349
+ delta.time_part()
1350
+ )
1351
+ except ToNanosecondsError as error:
1352
+ raise ToNanosecondsError(delta=delta, months=error.months) from None
1353
+ case never:
1354
+ assert_never(never)
369
1355
 
370
1356
 
371
1357
  @dataclass(kw_only=True, slots=True)
372
- class ParseTimeError(Exception):
373
- time: str
1358
+ class ToNanosecondsError(Exception):
1359
+ delta: DateOrDateTimeDelta
1360
+ months: int
374
1361
 
375
1362
  @override
376
1363
  def __str__(self) -> str:
377
- return f"Unable to parse time; got {self.time!r}"
1364
+ return f"Delta must not contain months; got {self.months}"
378
1365
 
379
1366
 
380
1367
  ##
381
1368
 
382
1369
 
383
- def parse_timedelta(timedelta: str, /) -> dt.timedelta:
384
- """Parse a string into a timedelta."""
385
- with suppress(ExtractGroupError):
386
- rest = extract_group(r"^-([\w\.]+)$", timedelta)
387
- return -parse_timedelta(rest)
388
- try:
389
- days_str, time_str = extract_groups(r"^P(?:(\d+)D)?(?:T([\w\.]*))?$", timedelta)
390
- except ExtractGroupsError:
391
- raise _ParseTimedeltaParseError(timedelta=timedelta) from None
392
- days = ZERO_TIME if days_str == "" else dt.timedelta(days=int(days_str))
393
- if time_str == "":
394
- time = ZERO_TIME
395
- else:
396
- time_part = DateTimeDelta.parse_common_iso(f"PT{time_str}").time_part()
397
- _, nanoseconds = divmod(time_part.in_nanoseconds(), 1000)
398
- if nanoseconds != 0:
399
- raise _ParseTimedeltaNanosecondError(
400
- timedelta=timedelta, nanoseconds=nanoseconds
401
- )
402
- time = dt.timedelta(microseconds=int(time_part.in_microseconds()))
403
- return days + time
1370
+ @overload
1371
+ def to_py_date_or_date_time(date_or_date_time: Date, /) -> dt.date: ...
1372
+ @overload
1373
+ def to_py_date_or_date_time(date_or_date_time: ZonedDateTime, /) -> dt.datetime: ...
1374
+ @overload
1375
+ def to_py_date_or_date_time(date_or_date_time: None, /) -> None: ...
1376
+ def to_py_date_or_date_time(
1377
+ date_or_date_time: Date | ZonedDateTime | None, /
1378
+ ) -> dt.date | None:
1379
+ """Convert a Date or ZonedDateTime into a standard library equivalent."""
1380
+ match date_or_date_time:
1381
+ case Date() as date:
1382
+ return date.py_date()
1383
+ case ZonedDateTime() as date_time:
1384
+ return date_time.py_datetime()
1385
+ case None:
1386
+ return None
1387
+ case never:
1388
+ assert_never(never)
404
1389
 
405
1390
 
406
- @dataclass(kw_only=True, slots=True)
407
- class ParseTimedeltaError(Exception):
408
- timedelta: str
1391
+ ##
409
1392
 
410
1393
 
411
- @dataclass(kw_only=True, slots=True)
412
- class _ParseTimedeltaParseError(ParseTimedeltaError):
413
- @override
414
- def __str__(self) -> str:
415
- return f"Unable to parse timedelta; got {self.timedelta!r}"
1394
+ @overload
1395
+ def to_py_time_delta(delta: Delta, /) -> dt.timedelta: ...
1396
+ @overload
1397
+ def to_py_time_delta(delta: None, /) -> None: ...
1398
+ def to_py_time_delta(delta: Delta | None, /) -> dt.timedelta | None:
1399
+ """Try convert a DateDelta to a standard library timedelta."""
1400
+ match delta:
1401
+ case DateDelta():
1402
+ return dt.timedelta(days=to_days(delta))
1403
+ case TimeDelta():
1404
+ nanos = delta.in_nanoseconds()
1405
+ micros, remainder = divmod(nanos, 1000)
1406
+ if remainder != 0:
1407
+ raise ToPyTimeDeltaError(nanoseconds=remainder)
1408
+ return dt.timedelta(microseconds=micros)
1409
+ case DateTimeDelta():
1410
+ return to_py_time_delta(delta.date_part()) + to_py_time_delta(
1411
+ delta.time_part()
1412
+ )
1413
+ case None:
1414
+ return None
1415
+ case never:
1416
+ assert_never(never)
416
1417
 
417
1418
 
418
1419
  @dataclass(kw_only=True, slots=True)
419
- class _ParseTimedeltaNanosecondError(ParseTimedeltaError):
1420
+ class ToPyTimeDeltaError(Exception):
420
1421
  nanoseconds: int
421
1422
 
422
1423
  @override
423
1424
  def __str__(self) -> str:
424
- return f"Unable to parse timedelta; got {self.nanoseconds} nanoseconds"
1425
+ return f"Time delta must not contain nanoseconds; got {self.nanoseconds}"
425
1426
 
426
1427
 
427
1428
  ##
428
1429
 
429
1430
 
430
- def parse_zoned_datetime(datetime: str, /) -> dt.datetime:
431
- """Parse a string into a zoned datetime."""
432
- try:
433
- zdt = ZonedDateTime.parse_common_iso(datetime)
434
- except ValueError:
435
- raise ParseZonedDateTimeError(datetime=datetime) from None
436
- return zdt.py_datetime()
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): ...
437
1461
 
438
1462
 
439
1463
  @dataclass(kw_only=True, slots=True)
440
- class ParseZonedDateTimeError(Exception):
441
- datetime: str
1464
+ class _ToSecondsMonthsError(ToSecondsError):
1465
+ delta: DateOrDateTimeDelta
1466
+ months: int
442
1467
 
443
1468
  @override
444
1469
  def __str__(self) -> str:
445
- return f"Unable to parse zoned datetime; got {self.datetime!r}"
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}"
446
1481
 
447
1482
 
448
1483
  ##
449
1484
 
450
1485
 
451
- def serialize_date(date: dt.date, /) -> str:
452
- """Serialize a date."""
453
- check_date_not_datetime(date)
454
- return Date.from_py_date(date).format_common_iso()
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)
455
1515
 
456
1516
 
457
1517
  ##
458
1518
 
459
1519
 
460
- def serialize_datetime(datetime: dt.datetime, /) -> str:
461
- """Serialize a datetime."""
462
- try:
463
- return serialize_plain_datetime(datetime)
464
- except SerializePlainDateTimeError:
465
- return serialize_zoned_datetime(datetime)
1520
+ def to_time_delta(nanos: int, /) -> TimeDelta:
1521
+ """Construct a time delta."""
1522
+ components = _to_time_delta_components(nanos)
1523
+ return TimeDelta(
1524
+ hours=components.hours,
1525
+ minutes=components.minutes,
1526
+ seconds=components.seconds,
1527
+ microseconds=components.microseconds,
1528
+ milliseconds=components.milliseconds,
1529
+ nanoseconds=components.nanoseconds,
1530
+ )
1531
+
1532
+
1533
+ @dataclass(kw_only=True, slots=True)
1534
+ class _TimeDeltaComponents:
1535
+ hours: int
1536
+ minutes: int
1537
+ seconds: int
1538
+ microseconds: int
1539
+ milliseconds: int
1540
+ nanoseconds: int
1541
+
1542
+
1543
+ def _to_time_delta_components(nanos: int, /) -> _TimeDeltaComponents:
1544
+ sign_use = sign(nanos)
1545
+ micros, nanos = divmod(nanos, int(1e3))
1546
+ millis, micros = divmod(micros, int(1e3))
1547
+ secs, millis = divmod(millis, int(1e3))
1548
+ mins, secs = divmod(secs, 60)
1549
+ hours, mins = divmod(mins, 60)
1550
+ match sign_use: # pragma: no cover
1551
+ case 1:
1552
+ if nanos < 0:
1553
+ nanos += int(1e3)
1554
+ micros -= 1
1555
+ if micros < 0:
1556
+ micros += int(1e3)
1557
+ millis -= 1
1558
+ if millis < 0:
1559
+ millis += int(1e3)
1560
+ secs -= 1
1561
+ if secs < 0:
1562
+ secs += 60
1563
+ mins -= 1
1564
+ if mins < 0:
1565
+ mins += 60
1566
+ hours -= 1
1567
+ case -1:
1568
+ if nanos > 0:
1569
+ nanos -= int(1e3)
1570
+ micros += 1
1571
+ if micros > 0:
1572
+ micros -= int(1e3)
1573
+ millis += 1
1574
+ if millis > 0:
1575
+ millis -= int(1e3)
1576
+ secs += 1
1577
+ if secs > 0:
1578
+ secs -= 60
1579
+ mins += 1
1580
+ if mins > 0:
1581
+ mins -= 60
1582
+ hours += 1
1583
+ case 0:
1584
+ ...
1585
+ return _TimeDeltaComponents(
1586
+ hours=hours,
1587
+ minutes=mins,
1588
+ seconds=secs,
1589
+ microseconds=micros,
1590
+ milliseconds=millis,
1591
+ nanoseconds=nanos,
1592
+ )
466
1593
 
467
1594
 
468
1595
  ##
469
1596
 
470
1597
 
471
- def serialize_duration(duration: Duration, /) -> str:
472
- """Serialize a duration."""
473
- if isinstance(duration, int | float):
474
- return str(duration)
1598
+ def to_weeks(delta: Delta, /) -> int:
1599
+ """Compute the number of weeks in a delta."""
475
1600
  try:
476
- return serialize_timedelta(duration)
477
- except SerializeTimeDeltaError as error:
478
- raise SerializeDurationError(duration=error.timedelta) from None
1601
+ days = to_days(delta)
1602
+ except _ToDaysMonthsError as error:
1603
+ raise _ToWeeksMonthsError(delta=error.delta, months=error.months) from None
1604
+ except _ToDaysNanosecondsError as error:
1605
+ raise _ToWeeksNanosecondsError(
1606
+ delta=error.delta, nanoseconds=error.nanoseconds
1607
+ ) from None
1608
+ weeks, remainder = divmod(days, 7)
1609
+ if remainder != 0:
1610
+ raise _ToWeeksDaysError(delta=delta, days=remainder) from None
1611
+ return weeks
479
1612
 
480
1613
 
481
1614
  @dataclass(kw_only=True, slots=True)
482
- class SerializeDurationError(Exception):
483
- duration: Duration
1615
+ class ToWeeksError(Exception): ...
1616
+
1617
+
1618
+ @dataclass(kw_only=True, slots=True)
1619
+ class _ToWeeksMonthsError(ToWeeksError):
1620
+ delta: DateOrDateTimeDelta
1621
+ months: int
484
1622
 
485
1623
  @override
486
1624
  def __str__(self) -> str:
487
- return f"Unable to serialize duration; got {self.duration}"
1625
+ return f"Delta must not contain months; got {self.months}"
488
1626
 
489
1627
 
490
- ##
491
-
1628
+ @dataclass(kw_only=True, slots=True)
1629
+ class _ToWeeksNanosecondsError(ToWeeksError):
1630
+ delta: TimeOrDateTimeDelta
1631
+ nanoseconds: int
492
1632
 
493
- def serialize_plain_datetime(datetime: dt.datetime, /) -> str:
494
- """Serialize a plain datetime."""
495
- try:
496
- pdt = PlainDateTime.from_py_datetime(datetime)
497
- except ValueError:
498
- raise SerializePlainDateTimeError(datetime=datetime) from None
499
- return pdt.format_common_iso()
1633
+ @override
1634
+ def __str__(self) -> str:
1635
+ return f"Delta must not contain extra nanoseconds; got {self.nanoseconds}"
500
1636
 
501
1637
 
502
1638
  @dataclass(kw_only=True, slots=True)
503
- class SerializePlainDateTimeError(Exception):
504
- datetime: dt.datetime
1639
+ class _ToWeeksDaysError(ToWeeksError):
1640
+ delta: Delta
1641
+ days: int
505
1642
 
506
1643
  @override
507
1644
  def __str__(self) -> str:
508
- return f"Unable to serialize plain datetime; got {self.datetime}"
1645
+ return f"Delta must not contain extra days; got {self.days}"
509
1646
 
510
1647
 
511
1648
  ##
512
1649
 
513
1650
 
514
- def serialize_time(time: dt.time, /) -> str:
515
- """Serialize a time."""
516
- return Time.from_py_time(time).format_common_iso()
1651
+ def to_years(delta: DateOrDateTimeDelta, /) -> int:
1652
+ """Compute the number of years in a delta."""
1653
+ match delta:
1654
+ case DateDelta():
1655
+ years, months, days = delta.in_years_months_days()
1656
+ if months != 0:
1657
+ raise _ToYearsMonthsError(delta=delta, months=months)
1658
+ if days != 0:
1659
+ raise _ToYearsDaysError(delta=delta, days=days)
1660
+ return years
1661
+ case DateTimeDelta():
1662
+ if delta.time_part() != TimeDelta():
1663
+ raise _ToYearsTimeError(delta=delta)
1664
+ try:
1665
+ return to_years(delta.date_part())
1666
+ except _ToYearsMonthsError as error:
1667
+ raise _ToYearsMonthsError(delta=delta, months=error.months) from None
1668
+ except _ToYearsDaysError as error:
1669
+ raise _ToYearsDaysError(delta=delta, days=error.days) from None
1670
+ case never:
1671
+ assert_never(never)
517
1672
 
518
1673
 
519
- ##
1674
+ @dataclass(kw_only=True, slots=True)
1675
+ class ToYearsError(Exception): ...
520
1676
 
521
1677
 
522
- def serialize_timedelta(timedelta: dt.timedelta, /) -> str:
523
- """Serialize a timedelta."""
524
- try:
525
- dtd = _to_datetime_delta(timedelta)
526
- except _ToDateTimeDeltaError as error:
527
- raise SerializeTimeDeltaError(timedelta=error.timedelta) from None
528
- return dtd.format_common_iso()
1678
+ @dataclass(kw_only=True, slots=True)
1679
+ class _ToYearsMonthsError(ToYearsError):
1680
+ delta: DateOrDateTimeDelta
1681
+ months: int
1682
+
1683
+ @override
1684
+ def __str__(self) -> str:
1685
+ return f"Delta must not contain months; got {self.months}"
1686
+
1687
+
1688
+ @dataclass(kw_only=True, slots=True)
1689
+ class _ToYearsDaysError(ToYearsError):
1690
+ delta: DateOrDateTimeDelta
1691
+ days: int
1692
+
1693
+ @override
1694
+ def __str__(self) -> str:
1695
+ return f"Delta must not contain days; got {self.days}"
529
1696
 
530
1697
 
531
1698
  @dataclass(kw_only=True, slots=True)
532
- class SerializeTimeDeltaError(Exception):
533
- timedelta: dt.timedelta
1699
+ class _ToYearsTimeError(ToYearsError):
1700
+ delta: DateTimeDelta
534
1701
 
535
1702
  @override
536
1703
  def __str__(self) -> str:
537
- return f"Unable to serialize timedelta; got {self.timedelta}"
1704
+ return f"Delta must not contain a time part; got {self.delta.time_part()}"
538
1705
 
539
1706
 
540
1707
  ##
541
1708
 
542
1709
 
543
- def serialize_zoned_datetime(datetime: dt.datetime, /) -> str:
544
- """Serialize a zoned datetime."""
545
- if datetime.tzinfo is dt.UTC:
546
- return serialize_zoned_datetime( # skipif-ci-and-windows
547
- datetime.replace(tzinfo=UTC)
548
- )
549
- try:
550
- zdt = ZonedDateTime.from_py_datetime(datetime)
551
- except ValueError:
552
- raise SerializeZonedDateTimeError(datetime=datetime) from None
553
- return zdt.format_common_iso()
1710
+ @overload
1711
+ def to_zoned_date_time(
1712
+ date_time: Sentinel, /, *, time_zone: TimeZoneLike | None = None
1713
+ ) -> Sentinel: ...
1714
+ @overload
1715
+ def to_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."""
1728
+ match 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)
1745
+ case Callable() as func:
1746
+ return to_zoned_date_time(func(), time_zone=time_zone)
1747
+ case never:
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))
554
1752
 
555
1753
 
556
1754
  @dataclass(kw_only=True, slots=True)
557
- class SerializeZonedDateTimeError(Exception):
558
- datetime: dt.datetime
1755
+ class ToZonedDateTimeError(Exception):
1756
+ date_time: dt.datetime
559
1757
 
560
1758
  @override
561
1759
  def __str__(self) -> str:
562
- return f"Unable to serialize zoned datetime; got {self.datetime}"
1760
+ return f"Expected date-time to have a `ZoneInfo` or `dt.UTC` as its timezone; got {self.date_time.tzinfo}"
1761
+
1762
+
1763
+ ##
1764
+
1765
+
1766
+ def two_digit_year_month(year: int, month: int, /) -> YearMonth:
1767
+ """Construct a year-month from a 2-digit year."""
1768
+ min_year = DATE_TWO_DIGIT_YEAR_MIN.year
1769
+ max_year = DATE_TWO_DIGIT_YEAR_MAX.year
1770
+ years = range(min_year, max_year + 1)
1771
+ (year_use,) = (y for y in years if y % 100 == year)
1772
+ return YearMonth(year_use, month)
563
1773
 
564
1774
 
565
1775
  ##
@@ -587,112 +1797,292 @@ class WheneverLogRecord(LogRecord):
587
1797
  name, level, pathname, lineno, msg, args, exc_info, func, sinfo
588
1798
  )
589
1799
  length = self._get_length()
590
- plain = format(self._get_now().to_plain().format_common_iso(), f"{length}s")
591
- time_zone = self._get_time_zone_key()
592
- self.zoned_datetime = f"{plain}[{time_zone}]"
593
-
594
- @classmethod
595
- @cache
596
- def _get_time_zone(cls) -> ZoneInfo:
597
- """Get the local timezone."""
598
- try:
599
- from utilities.tzlocal import get_local_time_zone
600
- except ModuleNotFoundError: # pragma: no cover
601
- return UTC
602
- return get_local_time_zone()
603
-
604
- @classmethod
605
- @cache
606
- def _get_time_zone_key(cls) -> str:
607
- """Get the local timezone as a string."""
608
- 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}]"
609
1802
 
610
1803
  @classmethod
611
1804
  @cache
612
1805
  def _get_length(cls) -> int:
613
1806
  """Get maximum length of a formatted string."""
614
- now = cls._get_now().replace(nanosecond=1000).to_plain()
615
- return len(now.format_common_iso())
1807
+ now = get_now_local().replace(nanosecond=1000).to_plain()
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')}"
616
1914
 
617
1915
  @classmethod
618
- def _get_now(cls) -> ZonedDateTime:
619
- """Get the current zoned datetime."""
620
- return ZonedDateTime.now(cls._get_time_zone().key)
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)
621
1944
 
1945
+ @property
1946
+ def time_zone(self) -> ZoneInfo:
1947
+ """The time zone of the period."""
1948
+ return ZoneInfo(self.start.tz)
622
1949
 
623
- ##
1950
+ def to_dict(self) -> PeriodDict[ZonedDateTime]:
1951
+ """Convert the period to a dictionary."""
1952
+ return PeriodDict(start=self.start, end=self.end)
624
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())
625
1957
 
626
- def _to_datetime_delta(timedelta: dt.timedelta, /) -> DateTimeDelta:
627
- """Serialize a timedelta."""
628
- total_microseconds = datetime_duration_to_microseconds(timedelta)
629
- if total_microseconds == 0:
630
- return DateTimeDelta()
631
- if total_microseconds >= 1:
632
- days, remainder = divmod(total_microseconds, _MICROSECONDS_PER_DAY)
633
- seconds, microseconds = divmod(remainder, _MICROSECONDS_PER_SECOND)
634
- try:
635
- dtd = DateTimeDelta(days=days, seconds=seconds, microseconds=microseconds)
636
- except (OverflowError, ValueError):
637
- raise _ToDateTimeDeltaError(timedelta=timedelta) from None
638
- months, days, seconds, nanoseconds = dtd.in_months_days_secs_nanos()
639
- return DateTimeDelta(
640
- months=months, days=days, seconds=seconds, nanoseconds=nanoseconds
641
- )
642
- return -_to_datetime_delta(-timedelta)
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}"
643
1988
 
644
1989
 
645
1990
  @dataclass(kw_only=True, slots=True)
646
- class _ToDateTimeDeltaError(Exception):
647
- timedelta: dt.timedelta
1991
+ class _ZonedDateTimePeriodExactEqError(ZonedDateTimePeriodError):
1992
+ args: tuple[Any, ...]
648
1993
 
649
1994
  @override
650
1995
  def __str__(self) -> str:
651
- return f"Unable to create DateTimeDelta; got {self.timedelta}"
1996
+ return f"Invalid arguments; got {self.args}"
652
1997
 
653
1998
 
654
1999
  __all__ = [
655
- "MAX_SERIALIZABLE_TIMEDELTA",
656
- "MIN_SERIALIZABLE_TIMEDELTA",
657
- "CheckValidZonedDateTimeError",
658
- "EnsureDateError",
659
- "EnsureDateTimeError",
660
- "EnsurePlainDateTimeError",
661
- "EnsureTimeError",
662
- "EnsureTimedeltaError",
663
- "EnsureZonedDateTimeError",
664
- "ParseDateError",
665
- "ParseDateTimeError",
666
- "ParseDurationError",
667
- "ParsePlainDateTimeError",
668
- "ParseTimeError",
669
- "ParseTimedeltaError",
670
- "ParseZonedDateTimeError",
671
- "SerializeDurationError",
672
- "SerializePlainDateTimeError",
673
- "SerializeTimeDeltaError",
674
- "SerializeZonedDateTimeError",
2000
+ "DATE_DELTA_MAX",
2001
+ "DATE_DELTA_MIN",
2002
+ "DATE_DELTA_PARSABLE_MAX",
2003
+ "DATE_DELTA_PARSABLE_MIN",
2004
+ "DATE_TIME_DELTA_MAX",
2005
+ "DATE_TIME_DELTA_MIN",
2006
+ "DATE_TIME_DELTA_PARSABLE_MAX",
2007
+ "DATE_TIME_DELTA_PARSABLE_MIN",
2008
+ "DATE_TWO_DIGIT_YEAR_MAX",
2009
+ "DATE_TWO_DIGIT_YEAR_MIN",
2010
+ "DAY",
2011
+ "HOUR",
2012
+ "MICROSECOND",
2013
+ "MILLISECOND",
2014
+ "MINUTE",
2015
+ "MONTH",
2016
+ "NOW_LOCAL",
2017
+ "NOW_LOCAL_PLAIN",
2018
+ "NOW_PLAIN",
2019
+ "SECOND",
2020
+ "TIME_DELTA_MAX",
2021
+ "TIME_DELTA_MIN",
2022
+ "TIME_LOCAL",
2023
+ "TIME_UTC",
2024
+ "TODAY_LOCAL",
2025
+ "TODAY_UTC",
2026
+ "WEEK",
2027
+ "YEAR",
2028
+ "ZERO_DAYS",
2029
+ "ZERO_TIME",
2030
+ "ZONED_DATE_TIME_MAX",
2031
+ "ZONED_DATE_TIME_MIN",
2032
+ "DatePeriod",
2033
+ "DatePeriodError",
2034
+ "MeanDateTimeError",
2035
+ "MinMaxDateError",
2036
+ "PeriodDict",
2037
+ "RoundDateOrDateTimeError",
2038
+ "TimePeriod",
2039
+ "ToDaysError",
2040
+ "ToMinutesError",
2041
+ "ToMonthsAndDaysError",
2042
+ "ToMonthsError",
2043
+ "ToNanosecondsError",
2044
+ "ToPyTimeDeltaError",
2045
+ "ToSecondsError",
2046
+ "ToWeeksError",
2047
+ "ToYearsError",
675
2048
  "WheneverLogRecord",
676
- "check_valid_zoned_datetime",
677
- "ensure_date",
678
- "ensure_datetime",
679
- "ensure_duration",
680
- "ensure_plain_datetime",
681
- "ensure_time",
682
- "ensure_timedelta",
683
- "ensure_zoned_datetime",
684
- "parse_date",
685
- "parse_datetime",
686
- "parse_duration",
687
- "parse_plain_datetime",
688
- "parse_time",
689
- "parse_timedelta",
690
- "parse_zoned_datetime",
691
- "serialize_date",
692
- "serialize_datetime",
693
- "serialize_duration",
694
- "serialize_plain_datetime",
695
- "serialize_time",
696
- "serialize_timedelta",
697
- "serialize_zoned_datetime",
2049
+ "ZonedDateTimePeriod",
2050
+ "ZonedDateTimePeriodError",
2051
+ "add_year_month",
2052
+ "datetime_utc",
2053
+ "diff_year_month",
2054
+ "format_compact",
2055
+ "from_timestamp",
2056
+ "from_timestamp_millis",
2057
+ "from_timestamp_nanos",
2058
+ "get_now",
2059
+ "get_now_local",
2060
+ "get_now_local_plain",
2061
+ "get_now_plain",
2062
+ "get_time",
2063
+ "get_time_local",
2064
+ "get_today",
2065
+ "get_today_local",
2066
+ "is_weekend",
2067
+ "mean_datetime",
2068
+ "min_max_date",
2069
+ "round_date_or_date_time",
2070
+ "sub_year_month",
2071
+ "to_date",
2072
+ "to_date_time_delta",
2073
+ "to_days",
2074
+ "to_microseconds",
2075
+ "to_milliseconds",
2076
+ "to_minutes",
2077
+ "to_months",
2078
+ "to_months_and_days",
2079
+ "to_nanoseconds",
2080
+ "to_py_date_or_date_time",
2081
+ "to_py_time_delta",
2082
+ "to_seconds",
2083
+ "to_time",
2084
+ "to_weeks",
2085
+ "to_years",
2086
+ "to_zoned_date_time",
2087
+ "two_digit_year_month",
698
2088
  ]