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/datetime.py DELETED
@@ -1,1409 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import datetime as dt
4
- from collections.abc import Callable, Iterable, Sequence
5
- from dataclasses import dataclass, replace
6
- from re import search, sub
7
- from statistics import fmean
8
- from typing import (
9
- TYPE_CHECKING,
10
- Any,
11
- Literal,
12
- Self,
13
- SupportsFloat,
14
- TypeGuard,
15
- assert_never,
16
- overload,
17
- override,
18
- )
19
-
20
- from utilities.iterables import OneEmptyError, one
21
- from utilities.math import SafeRoundError, round_, safe_round
22
- from utilities.platform import SYSTEM
23
- from utilities.sentinel import Sentinel, sentinel
24
- from utilities.types import MaybeStr
25
- from utilities.typing import is_instance_gen
26
- from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
27
-
28
- if TYPE_CHECKING:
29
- from collections.abc import Iterator
30
-
31
- from utilities.types import (
32
- DateOrDateTime,
33
- Duration,
34
- MaybeCallableDate,
35
- MaybeCallableDateTime,
36
- RoundMode,
37
- TimeZoneLike,
38
- )
39
-
40
-
41
- _DAYS_PER_YEAR = 365.25
42
- _MICROSECONDS_PER_MILLISECOND = int(1e3)
43
- _MICROSECONDS_PER_SECOND = int(1e6)
44
- _SECONDS_PER_DAY = 24 * 60 * 60
45
- _MICROSECONDS_PER_DAY = _MICROSECONDS_PER_SECOND * _SECONDS_PER_DAY
46
- DATETIME_MIN_UTC = dt.datetime.min.replace(tzinfo=UTC)
47
- DATETIME_MAX_UTC = dt.datetime.max.replace(tzinfo=UTC)
48
- DATETIME_MIN_NAIVE = DATETIME_MIN_UTC.replace(tzinfo=None)
49
- DATETIME_MAX_NAIVE = DATETIME_MAX_UTC.replace(tzinfo=None)
50
- EPOCH_UTC = dt.datetime.fromtimestamp(0, tz=UTC)
51
- EPOCH_DATE = EPOCH_UTC.date()
52
- EPOCH_NAIVE = EPOCH_UTC.replace(tzinfo=None)
53
- ZERO_TIME = dt.timedelta(0)
54
- MICROSECOND = dt.timedelta(microseconds=1)
55
- MILLISECOND = dt.timedelta(milliseconds=1)
56
- SECOND = dt.timedelta(seconds=1)
57
- MINUTE = dt.timedelta(minutes=1)
58
- HOUR = dt.timedelta(hours=1)
59
- DAY = dt.timedelta(days=1)
60
- WEEK = dt.timedelta(weeks=1)
61
-
62
-
63
- ##
64
-
65
-
66
- @overload
67
- def add_duration(
68
- date: dt.datetime, /, *, duration: Duration | None = ...
69
- ) -> dt.datetime: ...
70
- @overload
71
- def add_duration(date: dt.date, /, *, duration: Duration | None = ...) -> dt.date: ...
72
- def add_duration(
73
- date: DateOrDateTime, /, *, duration: Duration | None = None
74
- ) -> dt.date:
75
- """Add a duration to a date/datetime."""
76
- if duration is None:
77
- return date
78
- if isinstance(date, dt.datetime):
79
- return date + datetime_duration_to_timedelta(duration)
80
- try:
81
- timedelta = date_duration_to_timedelta(duration)
82
- except DateDurationToTimeDeltaError:
83
- raise AddDurationError(date=date, duration=duration) from None
84
- return date + timedelta
85
-
86
-
87
- @dataclass(kw_only=True, slots=True)
88
- class AddDurationError(Exception):
89
- date: dt.date
90
- duration: Duration
91
-
92
- @override
93
- def __str__(self) -> str:
94
- return f"Date {self.date} must be paired with an integral duration; got {self.duration}"
95
-
96
-
97
- ##
98
-
99
-
100
- def add_weekdays(date: dt.date, /, *, n: int = 1) -> dt.date:
101
- """Add a number of a weekdays to a given date.
102
-
103
- If the initial date is a weekend, then moving to the adjacent weekday
104
- counts as 1 move.
105
- """
106
- check_date_not_datetime(date)
107
- if n == 0 and not is_weekday(date):
108
- raise AddWeekdaysError(date)
109
- if n >= 1:
110
- for _ in range(n):
111
- date = round_to_next_weekday(date + DAY)
112
- elif n <= -1:
113
- for _ in range(-n):
114
- date = round_to_prev_weekday(date - DAY)
115
- return date
116
-
117
-
118
- class AddWeekdaysError(Exception): ...
119
-
120
-
121
- ##
122
-
123
-
124
- def are_equal_date_durations(x: Duration, y: Duration, /) -> bool:
125
- """Check if x == y for durations."""
126
- x_timedelta = date_duration_to_timedelta(x)
127
- y_timedelta = date_duration_to_timedelta(y)
128
- return x_timedelta == y_timedelta
129
-
130
-
131
- ##
132
-
133
-
134
- def are_equal_dates_or_datetimes(
135
- x: DateOrDateTime, y: DateOrDateTime, /, *, strict: bool = False
136
- ) -> bool:
137
- """Check if x == y for dates/datetimes."""
138
- if is_instance_gen(x, dt.date) and is_instance_gen(y, dt.date):
139
- return x == y
140
- if is_instance_gen(x, dt.datetime) and is_instance_gen(y, dt.datetime):
141
- return are_equal_datetimes(x, y, strict=strict)
142
- raise AreEqualDatesOrDateTimesError(x=x, y=y)
143
-
144
-
145
- @dataclass(kw_only=True, slots=True)
146
- class AreEqualDatesOrDateTimesError(Exception):
147
- x: DateOrDateTime
148
- y: DateOrDateTime
149
-
150
- @override
151
- def __str__(self) -> str:
152
- return f"Cannot compare date and datetime ({self.x}, {self.y})"
153
-
154
-
155
- ##
156
-
157
-
158
- def are_equal_datetime_durations(x: Duration, y: Duration, /) -> bool:
159
- """Check if x == y for durations."""
160
- x_timedelta = datetime_duration_to_timedelta(x)
161
- y_timedelta = datetime_duration_to_timedelta(y)
162
- return x_timedelta == y_timedelta
163
-
164
-
165
- ##
166
-
167
-
168
- def are_equal_datetimes(
169
- x: dt.datetime, y: dt.datetime, /, *, strict: bool = False
170
- ) -> bool:
171
- """Check if x == y for datetimes."""
172
- match x.tzinfo is None, y.tzinfo is None:
173
- case True, True:
174
- return x == y
175
- case False, False if x == y:
176
- return (x.tzinfo is y.tzinfo) or not strict
177
- case False, False if x != y:
178
- return False
179
- case _:
180
- raise AreEqualDateTimesError(x=x, y=y)
181
-
182
-
183
- @dataclass(kw_only=True, slots=True)
184
- class AreEqualDateTimesError(Exception):
185
- x: dt.datetime
186
- y: dt.datetime
187
-
188
- @override
189
- def __str__(self) -> str:
190
- return f"Cannot compare local and zoned datetimes ({self.x}, {self.y})"
191
-
192
-
193
- ##
194
-
195
-
196
- def are_equal_months(x: DateOrMonth, y: DateOrMonth, /) -> bool:
197
- """Check if x == y as months."""
198
- x_month = Month.from_date(x) if isinstance(x, dt.date) else x
199
- y_month = Month.from_date(y) if isinstance(y, dt.date) else y
200
- return x_month == y_month
201
-
202
-
203
- ##
204
-
205
-
206
- def check_date_not_datetime(date: dt.date, /) -> None:
207
- """Check if a date is not a datetime."""
208
- if not is_instance_gen(date, dt.date):
209
- raise CheckDateNotDateTimeError(date=date)
210
-
211
-
212
- @dataclass(kw_only=True, slots=True)
213
- class CheckDateNotDateTimeError(Exception):
214
- date: dt.date
215
-
216
- @override
217
- def __str__(self) -> str:
218
- return f"Date must not be a datetime; got {self.date}"
219
-
220
-
221
- ##
222
-
223
-
224
- def date_to_datetime(
225
- date: dt.date, /, *, time: dt.time | None = None, time_zone: TimeZoneLike = UTC
226
- ) -> dt.datetime:
227
- """Expand a date into a datetime."""
228
- check_date_not_datetime(date)
229
- time_use = dt.time(0) if time is None else time
230
- time_zone_use = ensure_time_zone(time_zone)
231
- return dt.datetime.combine(date, time_use, tzinfo=time_zone_use)
232
-
233
-
234
- ##
235
-
236
-
237
- def date_to_month(date: dt.date, /) -> Month:
238
- """Collapse a date into a month."""
239
- check_date_not_datetime(date)
240
- return Month(year=date.year, month=date.month)
241
-
242
-
243
- ##
244
-
245
-
246
- def date_duration_to_int(duration: Duration, /) -> int:
247
- """Ensure a date duration is a float."""
248
- match duration:
249
- case int():
250
- return duration
251
- case float():
252
- try:
253
- return safe_round(duration)
254
- except SafeRoundError:
255
- raise _DateDurationToIntFloatError(duration=duration) from None
256
- case dt.timedelta():
257
- if is_integral_timedelta(duration):
258
- return duration.days
259
- raise _DateDurationToIntTimeDeltaError(duration=duration) from None
260
- case _ as never:
261
- assert_never(never)
262
-
263
-
264
- @dataclass(kw_only=True, slots=True)
265
- class DateDurationToIntError(Exception):
266
- duration: Duration
267
-
268
-
269
- @dataclass(kw_only=True, slots=True)
270
- class _DateDurationToIntFloatError(DateDurationToIntError):
271
- @override
272
- def __str__(self) -> str:
273
- return f"Float duration must be integral; got {self.duration}"
274
-
275
-
276
- @dataclass(kw_only=True, slots=True)
277
- class _DateDurationToIntTimeDeltaError(DateDurationToIntError):
278
- @override
279
- def __str__(self) -> str:
280
- return f"Timedelta duration must be integral; got {self.duration}"
281
-
282
-
283
- def date_duration_to_timedelta(duration: Duration, /) -> dt.timedelta:
284
- """Ensure a date duration is a timedelta."""
285
- match duration:
286
- case int():
287
- return dt.timedelta(days=duration)
288
- case float():
289
- try:
290
- as_int = safe_round(duration)
291
- except SafeRoundError:
292
- raise _DateDurationToTimeDeltaFloatError(duration=duration) from None
293
- return dt.timedelta(days=as_int)
294
- case dt.timedelta():
295
- if is_integral_timedelta(duration):
296
- return duration
297
- raise _DateDurationToTimeDeltaTimeDeltaError(duration=duration) from None
298
- case _ as never:
299
- assert_never(never)
300
-
301
-
302
- @dataclass(kw_only=True, slots=True)
303
- class DateDurationToTimeDeltaError(Exception):
304
- duration: Duration
305
-
306
-
307
- @dataclass(kw_only=True, slots=True)
308
- class _DateDurationToTimeDeltaFloatError(DateDurationToTimeDeltaError):
309
- @override
310
- def __str__(self) -> str:
311
- return f"Float duration must be integral; got {self.duration}"
312
-
313
-
314
- @dataclass(kw_only=True, slots=True)
315
- class _DateDurationToTimeDeltaTimeDeltaError(DateDurationToTimeDeltaError):
316
- @override
317
- def __str__(self) -> str:
318
- return f"Timedelta duration must be integral; got {self.duration}"
319
-
320
-
321
- ##
322
-
323
-
324
- def datetime_duration_to_float(duration: Duration, /) -> float:
325
- """Ensure a datetime duration is a float."""
326
- match duration:
327
- case int():
328
- return float(duration)
329
- case float():
330
- return duration
331
- case dt.timedelta():
332
- return duration.total_seconds()
333
- case _ as never:
334
- assert_never(never)
335
-
336
-
337
- def datetime_duration_to_microseconds(duration: Duration, /) -> int:
338
- """Compute the number of microseconds in a datetime duration."""
339
- timedelta = datetime_duration_to_timedelta(duration)
340
- return (
341
- _MICROSECONDS_PER_DAY * timedelta.days
342
- + _MICROSECONDS_PER_SECOND * timedelta.seconds
343
- + timedelta.microseconds
344
- )
345
-
346
-
347
- @overload
348
- def datetime_duration_to_milliseconds(
349
- duration: Duration, /, *, strict: Literal[True]
350
- ) -> int: ...
351
- @overload
352
- def datetime_duration_to_milliseconds(
353
- duration: Duration, /, *, strict: bool = False
354
- ) -> float: ...
355
- def datetime_duration_to_milliseconds(
356
- duration: Duration, /, *, strict: bool = False
357
- ) -> int | float:
358
- """Compute the number of milliseconds in a datetime duration."""
359
- timedelta = datetime_duration_to_timedelta(duration)
360
- microseconds = datetime_duration_to_microseconds(timedelta)
361
- milliseconds, remainder = divmod(microseconds, _MICROSECONDS_PER_MILLISECOND)
362
- match remainder, strict:
363
- case 0, _:
364
- return milliseconds
365
- case _, True:
366
- raise TimedeltaToMillisecondsError(duration=duration, remainder=remainder)
367
- case _, False:
368
- return milliseconds + remainder / _MICROSECONDS_PER_MILLISECOND
369
- case _ as never:
370
- assert_never(never)
371
-
372
-
373
- @dataclass(kw_only=True, slots=True)
374
- class TimedeltaToMillisecondsError(Exception):
375
- duration: Duration
376
- remainder: int
377
-
378
- @override
379
- def __str__(self) -> str:
380
- return f"Unable to convert {self.duration} to milliseconds; got {self.remainder} microsecond(s)"
381
-
382
-
383
- def datetime_duration_to_timedelta(duration: Duration, /) -> dt.timedelta:
384
- """Ensure a datetime duration is a timedelta."""
385
- match duration:
386
- case int() | float():
387
- return dt.timedelta(seconds=duration)
388
- case dt.timedelta():
389
- return duration
390
- case _ as never:
391
- assert_never(never)
392
-
393
-
394
- ##
395
-
396
-
397
- def datetime_utc(
398
- year: int,
399
- month: int,
400
- day: int,
401
- /,
402
- hour: int = 0,
403
- minute: int = 0,
404
- second: int = 0,
405
- microsecond: int = 0,
406
- ) -> dt.datetime:
407
- """Create a UTC-zoned datetime."""
408
- return dt.datetime(
409
- year,
410
- month,
411
- day,
412
- hour=hour,
413
- minute=minute,
414
- second=second,
415
- microsecond=microsecond,
416
- tzinfo=UTC,
417
- )
418
-
419
-
420
- ##
421
-
422
-
423
- def days_since_epoch(date: dt.date, /) -> int:
424
- """Compute the number of days since the epoch."""
425
- check_date_not_datetime(date)
426
- return timedelta_since_epoch(date).days
427
-
428
-
429
- def days_since_epoch_to_date(days: int, /) -> dt.date:
430
- """Convert a number of days since the epoch to a date."""
431
- return EPOCH_DATE + days * DAY
432
-
433
-
434
- ##
435
-
436
-
437
- def ensure_month(month: MonthLike, /) -> Month:
438
- """Ensure the object is a month."""
439
- if isinstance(month, Month):
440
- return month
441
- try:
442
- return parse_month(month)
443
- except ParseMonthError as error:
444
- raise EnsureMonthError(month=error.month) from None
445
-
446
-
447
- @dataclass(kw_only=True, slots=True)
448
- class EnsureMonthError(Exception):
449
- month: str
450
-
451
- @override
452
- def __str__(self) -> str:
453
- return f"Unable to ensure month; got {self.month!r}"
454
-
455
-
456
- ##
457
-
458
-
459
- def format_datetime_local_and_utc(datetime: dt.datetime, /) -> str:
460
- """Format a plain datetime locally & in UTC."""
461
- time_zone = ensure_time_zone(datetime)
462
- if time_zone is UTC:
463
- return datetime.strftime("%Y-%m-%d %H:%M:%S (%a, UTC)")
464
- as_utc = datetime.astimezone(UTC)
465
- local = get_time_zone_name(time_zone)
466
- if datetime.year != as_utc.year:
467
- return f"{datetime:%Y-%m-%d %H:%M:%S (%a}, {local}, {as_utc:%Y-%m-%d %H:%M:%S} UTC)"
468
- if (datetime.month != as_utc.month) or (datetime.day != as_utc.day):
469
- return (
470
- f"{datetime:%Y-%m-%d %H:%M:%S (%a}, {local}, {as_utc:%m-%d %H:%M:%S} UTC)"
471
- )
472
- return f"{datetime:%Y-%m-%d %H:%M:%S (%a}, {local}, {as_utc:%H:%M:%S} UTC)"
473
-
474
-
475
- ##
476
-
477
-
478
- @overload
479
- def get_date(*, date: MaybeCallableDate) -> dt.date: ...
480
- @overload
481
- def get_date(*, date: None) -> None: ...
482
- @overload
483
- def get_date(*, date: Sentinel) -> Sentinel: ...
484
- @overload
485
- def get_date(*, date: MaybeCallableDate | Sentinel) -> dt.date | Sentinel: ...
486
- @overload
487
- def get_date(
488
- *, date: MaybeCallableDate | None | Sentinel = sentinel
489
- ) -> dt.date | None | Sentinel: ...
490
- def get_date(
491
- *, date: MaybeCallableDate | None | Sentinel = sentinel
492
- ) -> dt.date | None | Sentinel:
493
- """Get the date."""
494
- match date:
495
- case dt.date() | None | Sentinel():
496
- return date
497
- case Callable() as func:
498
- return get_date(date=func())
499
- case _ as never:
500
- assert_never(never)
501
-
502
-
503
- ##
504
-
505
-
506
- @overload
507
- def get_datetime(*, datetime: MaybeCallableDateTime) -> dt.datetime: ...
508
- @overload
509
- def get_datetime(*, datetime: None) -> None: ...
510
- @overload
511
- def get_datetime(*, datetime: Sentinel) -> Sentinel: ...
512
- def get_datetime(
513
- *, datetime: MaybeCallableDateTime | None | Sentinel = sentinel
514
- ) -> dt.datetime | None | Sentinel:
515
- """Get the datetime."""
516
- match datetime:
517
- case dt.datetime() | None | Sentinel():
518
- return datetime
519
- case Callable() as func:
520
- return get_datetime(datetime=func())
521
- case _ as never:
522
- assert_never(never)
523
-
524
-
525
- ##
526
-
527
-
528
- def get_half_years(*, n: int = 1) -> dt.timedelta:
529
- """Get a number of half-years as a timedelta."""
530
- days_per_half_year = _DAYS_PER_YEAR / 2
531
- return dt.timedelta(days=round(n * days_per_half_year))
532
-
533
-
534
- HALF_YEAR = get_half_years(n=1)
535
-
536
- ##
537
-
538
-
539
- def get_min_max_date(
540
- *,
541
- min_date: dt.date | None = None,
542
- max_date: dt.date | None = None,
543
- min_age: Duration | None = None,
544
- max_age: Duration | None = None,
545
- time_zone: TimeZoneLike = UTC,
546
- ) -> tuple[dt.date | None, dt.date | None]:
547
- """Get the min/max date given a combination of dates/ages."""
548
- today = get_today(time_zone=time_zone)
549
- min_parts: Sequence[dt.date] = []
550
- if min_date is not None:
551
- if min_date > today:
552
- raise _GetMinMaxDateMinDateError(min_date=min_date, today=today)
553
- min_parts.append(min_date)
554
- if max_age is not None:
555
- if date_duration_to_timedelta(max_age) < ZERO_TIME:
556
- raise _GetMinMaxDateMaxAgeError(max_age=max_age)
557
- min_parts.append(sub_duration(today, duration=max_age))
558
- min_date_use = max(min_parts, default=None)
559
- max_parts: Sequence[dt.date] = []
560
- if max_date is not None:
561
- if max_date > today:
562
- raise _GetMinMaxDateMaxDateError(max_date=max_date, today=today)
563
- max_parts.append(max_date)
564
- if min_age is not None:
565
- if date_duration_to_timedelta(min_age) < ZERO_TIME:
566
- raise _GetMinMaxDateMinAgeError(min_age=min_age)
567
- max_parts.append(sub_duration(today, duration=min_age))
568
- max_date_use = min(max_parts, default=None)
569
- if (
570
- (min_date_use is not None)
571
- and (max_date_use is not None)
572
- and (min_date_use > max_date_use)
573
- ):
574
- raise _GetMinMaxDatePeriodError(min_date=min_date_use, max_date=max_date_use)
575
- return min_date_use, max_date_use
576
-
577
-
578
- @dataclass(kw_only=True, slots=True)
579
- class GetMinMaxDateError(Exception): ...
580
-
581
-
582
- @dataclass(kw_only=True, slots=True)
583
- class _GetMinMaxDateMinDateError(GetMinMaxDateError):
584
- min_date: dt.date
585
- today: dt.date
586
-
587
- @override
588
- def __str__(self) -> str:
589
- return f"Min date must be at most today; got {self.min_date} > {self.today}"
590
-
591
-
592
- @dataclass(kw_only=True, slots=True)
593
- class _GetMinMaxDateMinAgeError(GetMinMaxDateError):
594
- min_age: Duration
595
-
596
- @override
597
- def __str__(self) -> str:
598
- return f"Min age must be non-negative; got {self.min_age}"
599
-
600
-
601
- @dataclass(kw_only=True, slots=True)
602
- class _GetMinMaxDateMaxDateError(GetMinMaxDateError):
603
- max_date: dt.date
604
- today: dt.date
605
-
606
- @override
607
- def __str__(self) -> str:
608
- return f"Max date must be at most today; got {self.max_date} > {self.today}"
609
-
610
-
611
- @dataclass(kw_only=True, slots=True)
612
- class _GetMinMaxDateMaxAgeError(GetMinMaxDateError):
613
- max_age: Duration
614
-
615
- @override
616
- def __str__(self) -> str:
617
- return f"Max age must be non-negative; got {self.max_age}"
618
-
619
-
620
- @dataclass(kw_only=True, slots=True)
621
- class _GetMinMaxDatePeriodError(GetMinMaxDateError):
622
- min_date: dt.date
623
- max_date: dt.date
624
-
625
- @override
626
- def __str__(self) -> str:
627
- return (
628
- f"Min date must be at most max date; got {self.min_date} > {self.max_date}"
629
- )
630
-
631
-
632
- ##
633
-
634
-
635
- def get_months(*, n: int = 1) -> dt.timedelta:
636
- """Get a number of months as a timedelta."""
637
- days_per_month = _DAYS_PER_YEAR / 12
638
- return dt.timedelta(days=round(n * days_per_month))
639
-
640
-
641
- MONTH = get_months(n=1)
642
-
643
-
644
- ##
645
-
646
-
647
- def get_now(*, time_zone: TimeZoneLike = UTC) -> dt.datetime:
648
- """Get the current, timezone-aware time."""
649
- return dt.datetime.now(tz=ensure_time_zone(time_zone))
650
-
651
-
652
- NOW_UTC = get_now(time_zone=UTC)
653
-
654
-
655
- ##
656
-
657
-
658
- def get_quarters(*, n: int = 1) -> dt.timedelta:
659
- """Get a number of quarters as a timedelta."""
660
- days_per_quarter = _DAYS_PER_YEAR / 4
661
- return dt.timedelta(days=round(n * days_per_quarter))
662
-
663
-
664
- QUARTER = get_quarters(n=1)
665
-
666
-
667
- ##
668
-
669
-
670
- def get_today(*, time_zone: TimeZoneLike = UTC) -> dt.date:
671
- """Get the current, timezone-aware date."""
672
- return get_now(time_zone=time_zone).date()
673
-
674
-
675
- TODAY_UTC = get_today(time_zone=UTC)
676
-
677
-
678
- ##
679
-
680
-
681
- def get_years(*, n: int = 1) -> dt.timedelta:
682
- """Get a number of years as a timedelta."""
683
- return dt.timedelta(days=round(n * _DAYS_PER_YEAR))
684
-
685
-
686
- YEAR = get_years(n=1)
687
-
688
-
689
- ##
690
-
691
-
692
- def is_integral_timedelta(duration: Duration, /) -> bool:
693
- """Check if a duration is integral."""
694
- timedelta = datetime_duration_to_timedelta(duration)
695
- return (timedelta.seconds == 0) and (timedelta.microseconds == 0)
696
-
697
-
698
- ##
699
-
700
-
701
- def is_plain_datetime(obj: Any, /) -> TypeGuard[dt.datetime]:
702
- """Check if an object is a plain datetime."""
703
- return isinstance(obj, dt.datetime) and (obj.tzinfo is None)
704
-
705
-
706
- ##
707
-
708
-
709
- _FRIDAY = 5
710
-
711
-
712
- def is_weekday(date: dt.date, /) -> bool:
713
- """Check if a date is a weekday."""
714
- check_date_not_datetime(date)
715
- return date.isoweekday() <= _FRIDAY
716
-
717
-
718
- ##
719
-
720
-
721
- def is_zero_time(duration: Duration, /) -> bool:
722
- """Check if a timedelta is 0."""
723
- return datetime_duration_to_timedelta(duration) == ZERO_TIME
724
-
725
-
726
- ##
727
-
728
-
729
- def is_zoned_datetime(obj: Any, /) -> TypeGuard[dt.datetime]:
730
- """Check if an object is a zoned datetime."""
731
- return isinstance(obj, dt.datetime) and (obj.tzinfo is not None)
732
-
733
-
734
- ##
735
-
736
-
737
- def maybe_sub_pct_y(text: str, /) -> str:
738
- """Substitute the `%Y' token with '%4Y' if necessary."""
739
- match SYSTEM:
740
- case "windows": # skipif-not-windows
741
- return text
742
- case "mac": # skipif-not-macos
743
- return text
744
- case "linux": # skipif-not-linux
745
- return sub("%Y", "%4Y", text)
746
- case _ as never:
747
- assert_never(never)
748
-
749
-
750
- ##
751
-
752
-
753
- def mean_datetime(
754
- datetimes: Iterable[dt.datetime],
755
- /,
756
- *,
757
- weights: Iterable[SupportsFloat] | None = None,
758
- mode: RoundMode = "standard",
759
- rel_tol: float | None = None,
760
- abs_tol: float | None = None,
761
- ) -> dt.datetime:
762
- """Compute the mean of a set of datetimes."""
763
- datetimes = list(datetimes)
764
- match len(datetimes):
765
- case 0:
766
- raise MeanDateTimeError from None
767
- case 1:
768
- return one(datetimes)
769
- case _:
770
- microseconds = list(map(microseconds_since_epoch, datetimes))
771
- mean_float = fmean(microseconds, weights=weights)
772
- mean_int = round_(mean_float, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol)
773
- return microseconds_since_epoch_to_datetime(
774
- mean_int, time_zone=datetimes[0].tzinfo
775
- )
776
-
777
-
778
- @dataclass(kw_only=True, slots=True)
779
- class MeanDateTimeError(Exception):
780
- @override
781
- def __str__(self) -> str:
782
- return "Mean requires at least 1 datetime"
783
-
784
-
785
- ##
786
-
787
-
788
- def mean_timedelta(
789
- timedeltas: Iterable[dt.timedelta],
790
- /,
791
- *,
792
- weights: Iterable[SupportsFloat] | None = None,
793
- mode: RoundMode = "standard",
794
- rel_tol: float | None = None,
795
- abs_tol: float | None = None,
796
- ) -> dt.timedelta:
797
- """Compute the mean of a set of timedeltas."""
798
- timedeltas = list(timedeltas)
799
- match len(timedeltas):
800
- case 0:
801
- raise MeanTimeDeltaError from None
802
- case 1:
803
- return one(timedeltas)
804
- case _:
805
- microseconds = list(map(datetime_duration_to_microseconds, timedeltas))
806
- mean_float = fmean(microseconds, weights=weights)
807
- mean_int = round_(mean_float, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol)
808
- return microseconds_to_timedelta(mean_int)
809
-
810
-
811
- @dataclass(kw_only=True, slots=True)
812
- class MeanTimeDeltaError(Exception):
813
- @override
814
- def __str__(self) -> str:
815
- return "Mean requires at least 1 timedelta"
816
-
817
-
818
- ##
819
-
820
-
821
- def microseconds_since_epoch(datetime: dt.datetime, /) -> int:
822
- """Compute the number of microseconds since the epoch."""
823
- return datetime_duration_to_microseconds(timedelta_since_epoch(datetime))
824
-
825
-
826
- def microseconds_to_timedelta(microseconds: int, /) -> dt.timedelta:
827
- """Compute a timedelta given a number of microseconds."""
828
- if microseconds == 0:
829
- return ZERO_TIME
830
- if microseconds >= 1:
831
- days, remainder = divmod(microseconds, _MICROSECONDS_PER_DAY)
832
- seconds, micros = divmod(remainder, _MICROSECONDS_PER_SECOND)
833
- return dt.timedelta(days=days, seconds=seconds, microseconds=micros)
834
- return -microseconds_to_timedelta(-microseconds)
835
-
836
-
837
- def microseconds_since_epoch_to_datetime(
838
- microseconds: int, /, *, time_zone: dt.tzinfo | None = None
839
- ) -> dt.datetime:
840
- """Convert a number of microseconds since the epoch to a datetime."""
841
- epoch = EPOCH_NAIVE if time_zone is None else EPOCH_UTC
842
- timedelta = microseconds_to_timedelta(microseconds)
843
- return epoch + timedelta
844
-
845
-
846
- ##
847
-
848
-
849
- @overload
850
- def milliseconds_since_epoch(
851
- datetime: dt.datetime, /, *, strict: Literal[True]
852
- ) -> int: ...
853
- @overload
854
- def milliseconds_since_epoch(
855
- datetime: dt.datetime, /, *, strict: bool = False
856
- ) -> float: ...
857
- def milliseconds_since_epoch(
858
- datetime: dt.datetime, /, *, strict: bool = False
859
- ) -> float:
860
- """Compute the number of milliseconds since the epoch."""
861
- microseconds = microseconds_since_epoch(datetime)
862
- milliseconds, remainder = divmod(microseconds, _MICROSECONDS_PER_MILLISECOND)
863
- if strict:
864
- if remainder == 0:
865
- return milliseconds
866
- raise MillisecondsSinceEpochError(datetime=datetime, remainder=remainder)
867
- return milliseconds + remainder / _MICROSECONDS_PER_MILLISECOND
868
-
869
-
870
- @dataclass(kw_only=True, slots=True)
871
- class MillisecondsSinceEpochError(Exception):
872
- datetime: dt.datetime
873
- remainder: int
874
-
875
- @override
876
- def __str__(self) -> str:
877
- return f"Unable to convert {self.datetime} to milliseconds since epoch; got {self.remainder} microsecond(s)"
878
-
879
-
880
- def milliseconds_since_epoch_to_datetime(
881
- milliseconds: int, /, *, time_zone: dt.tzinfo | None = None
882
- ) -> dt.datetime:
883
- """Convert a number of milliseconds since the epoch to a datetime."""
884
- epoch = EPOCH_NAIVE if time_zone is None else EPOCH_UTC
885
- timedelta = milliseconds_to_timedelta(milliseconds)
886
- return epoch + timedelta
887
-
888
-
889
- def milliseconds_to_timedelta(milliseconds: int, /) -> dt.timedelta:
890
- """Compute a timedelta given a number of milliseconds."""
891
- return microseconds_to_timedelta(_MICROSECONDS_PER_MILLISECOND * milliseconds)
892
-
893
-
894
- ##
895
-
896
-
897
- @dataclass(order=True, unsafe_hash=True, slots=True)
898
- class Month:
899
- """Represents a month in time."""
900
-
901
- year: int
902
- month: int
903
-
904
- def __post_init__(self) -> None:
905
- try:
906
- _ = dt.date(self.year, self.month, 1)
907
- except ValueError:
908
- raise MonthError(year=self.year, month=self.month) from None
909
-
910
- @override
911
- def __repr__(self) -> str:
912
- return serialize_month(self)
913
-
914
- @override
915
- def __str__(self) -> str:
916
- return repr(self)
917
-
918
- def __add__(self, other: Any, /) -> Self:
919
- if not isinstance(other, int): # pragma: no cover
920
- return NotImplemented
921
- years, month = divmod(self.month + other - 1, 12)
922
- month += 1
923
- year = self.year + years
924
- return replace(self, year=year, month=month)
925
-
926
- @overload
927
- def __sub__(self, other: Self, /) -> int: ...
928
- @overload
929
- def __sub__(self, other: int, /) -> Self: ...
930
- def __sub__(self, other: Self | int, /) -> Self | int:
931
- if isinstance(other, int): # pragma: no cover
932
- return self + (-other)
933
- if isinstance(other, type(self)):
934
- self_as_int = 12 * self.year + self.month
935
- other_as_int = 12 * other.year + other.month
936
- return self_as_int - other_as_int
937
- return NotImplemented # pragma: no cover
938
-
939
- @classmethod
940
- def from_date(cls, date: dt.date, /) -> Self:
941
- check_date_not_datetime(date)
942
- return cls(year=date.year, month=date.month)
943
-
944
- def to_date(self, /, *, day: int = 1) -> dt.date:
945
- return dt.date(self.year, self.month, day)
946
-
947
-
948
- @dataclass(kw_only=True, slots=True)
949
- class MonthError(Exception):
950
- year: int
951
- month: int
952
-
953
- @override
954
- def __str__(self) -> str:
955
- return f"Invalid year and month: {self.year}, {self.month}"
956
-
957
-
958
- type DateOrMonth = dt.date | Month
959
- type MonthLike = MaybeStr[Month]
960
- MIN_MONTH = Month(dt.date.min.year, dt.date.min.month)
961
- MAX_MONTH = Month(dt.date.max.year, dt.date.max.month)
962
-
963
-
964
- ##
965
-
966
-
967
- _TWO_DIGIT_YEAR_MIN = 1969
968
- _TWO_DIGIT_YEAR_MAX = _TWO_DIGIT_YEAR_MIN + 99
969
- MIN_DATE_TWO_DIGIT_YEAR = dt.date(
970
- _TWO_DIGIT_YEAR_MIN, dt.date.min.month, dt.date.min.day
971
- )
972
- MAX_DATE_TWO_DIGIT_YEAR = dt.date(
973
- _TWO_DIGIT_YEAR_MAX, dt.date.max.month, dt.date.max.day
974
- )
975
-
976
-
977
- def parse_two_digit_year(year: int | str, /) -> int:
978
- """Parse a 2-digit year into a year."""
979
- match year:
980
- case int():
981
- years = range(_TWO_DIGIT_YEAR_MIN, _TWO_DIGIT_YEAR_MAX + 1)
982
- try:
983
- return one(y for y in years if y % 100 == year)
984
- except OneEmptyError:
985
- raise _ParseTwoDigitYearInvalidIntegerError(year=year) from None
986
- case str():
987
- if search(r"^\d{1,2}$", year):
988
- return parse_two_digit_year(int(year))
989
- raise _ParseTwoDigitYearInvalidStringError(year=year)
990
- case _ as never:
991
- assert_never(never)
992
-
993
-
994
- @dataclass(kw_only=True, slots=True)
995
- class ParseTwoDigitYearError(Exception):
996
- year: int | str
997
-
998
-
999
- @dataclass(kw_only=True, slots=True)
1000
- class _ParseTwoDigitYearInvalidIntegerError(Exception):
1001
- year: int | str
1002
-
1003
- @override
1004
- def __str__(self) -> str:
1005
- return f"Unable to parse year; got {self.year!r}"
1006
-
1007
-
1008
- @dataclass(kw_only=True, slots=True)
1009
- class _ParseTwoDigitYearInvalidStringError(Exception):
1010
- year: int | str
1011
-
1012
- @override
1013
- def __str__(self) -> str:
1014
- return f"Unable to parse year; got {self.year!r}"
1015
-
1016
-
1017
- ##
1018
-
1019
-
1020
- def round_datetime(
1021
- datetime: dt.datetime,
1022
- duration: Duration,
1023
- /,
1024
- *,
1025
- mode: RoundMode = "standard",
1026
- rel_tol: float | None = None,
1027
- abs_tol: float | None = None,
1028
- ) -> dt.datetime:
1029
- """Round a datetime to a timedelta."""
1030
- if datetime.tzinfo is None:
1031
- dividend = microseconds_since_epoch(datetime)
1032
- divisor = datetime_duration_to_microseconds(duration)
1033
- quotient, remainder = divmod(dividend, divisor)
1034
- rnd_remainder = round_(
1035
- remainder / divisor, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
1036
- )
1037
- rnd_quotient = quotient + rnd_remainder
1038
- microseconds = rnd_quotient * divisor
1039
- return microseconds_since_epoch_to_datetime(microseconds)
1040
- local = datetime.replace(tzinfo=None)
1041
- rounded = round_datetime(
1042
- local, duration, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
1043
- )
1044
- return rounded.replace(tzinfo=datetime.tzinfo)
1045
-
1046
-
1047
- ##
1048
-
1049
-
1050
- def round_to_next_weekday(date: dt.date, /) -> dt.date:
1051
- """Round a date to the next weekday."""
1052
- return _round_to_weekday(date, prev_or_next="next")
1053
-
1054
-
1055
- def round_to_prev_weekday(date: dt.date, /) -> dt.date:
1056
- """Round a date to the previous weekday."""
1057
- return _round_to_weekday(date, prev_or_next="prev")
1058
-
1059
-
1060
- def _round_to_weekday(
1061
- date: dt.date, /, *, prev_or_next: Literal["prev", "next"]
1062
- ) -> dt.date:
1063
- """Round a date to the previous weekday."""
1064
- check_date_not_datetime(date)
1065
- match prev_or_next:
1066
- case "prev":
1067
- n = -1
1068
- case "next":
1069
- n = 1
1070
- case _ as never:
1071
- assert_never(never)
1072
- while not is_weekday(date):
1073
- date = add_weekdays(date, n=n)
1074
- return date
1075
-
1076
-
1077
- ##
1078
-
1079
-
1080
- def serialize_compact(date_or_datetime: DateOrDateTime, /) -> str:
1081
- """Serialize a date/datetime using a compact format."""
1082
- match date_or_datetime:
1083
- case dt.datetime() as datetime:
1084
- if datetime.tzinfo is None:
1085
- raise SerializeCompactError(datetime=datetime)
1086
- format_ = "%Y%m%dT%H%M%S"
1087
- case dt.date():
1088
- format_ = "%Y%m%d"
1089
- case _ as never:
1090
- assert_never(never)
1091
- return date_or_datetime.strftime(maybe_sub_pct_y(format_))
1092
-
1093
-
1094
- @dataclass(kw_only=True, slots=True)
1095
- class SerializeCompactError(Exception):
1096
- datetime: dt.datetime
1097
-
1098
- @override
1099
- def __str__(self) -> str:
1100
- return f"Unable to serialize plain datetime {self.datetime}"
1101
-
1102
-
1103
- def parse_date_compact(text: str, /) -> dt.date:
1104
- """Parse a compact string into a date."""
1105
- try:
1106
- datetime = dt.datetime.strptime(text, "%Y%m%d").replace(tzinfo=UTC)
1107
- except ValueError:
1108
- raise ParseDateCompactError(text=text) from None
1109
- return datetime.date()
1110
-
1111
-
1112
- @dataclass(kw_only=True, slots=True)
1113
- class ParseDateCompactError(Exception):
1114
- text: str
1115
-
1116
- @override
1117
- def __str__(self) -> str:
1118
- return f"Unable to parse {self.text!r} into a date"
1119
-
1120
-
1121
- def parse_datetime_compact(
1122
- text: str, /, *, time_zone: TimeZoneLike = UTC
1123
- ) -> dt.datetime:
1124
- """Parse a compact string into a datetime."""
1125
- time_zone = ensure_time_zone(time_zone)
1126
- try:
1127
- return dt.datetime.strptime(text, "%Y%m%dT%H%M%S").replace(tzinfo=time_zone)
1128
- except ValueError:
1129
- raise ParseDateTimeCompactError(text=text) from None
1130
-
1131
-
1132
- @dataclass(kw_only=True, slots=True)
1133
- class ParseDateTimeCompactError(Exception):
1134
- text: str
1135
-
1136
- @override
1137
- def __str__(self) -> str:
1138
- return f"Unable to parse {self.text!r} into a datetime"
1139
-
1140
-
1141
- ##
1142
-
1143
-
1144
- def serialize_month(month: Month, /) -> str:
1145
- """Serialize a month."""
1146
- return f"{month.year:04}-{month.month:02}"
1147
-
1148
-
1149
- def parse_month(month: str, /) -> Month:
1150
- """Parse a string into a month."""
1151
- for fmt in ["%Y-%m", "%Y%m", "%Y %m"]:
1152
- try:
1153
- date = dt.datetime.strptime(month, fmt).replace(tzinfo=UTC).date()
1154
- except ValueError:
1155
- pass
1156
- else:
1157
- return Month(date.year, date.month)
1158
- raise ParseMonthError(month=month)
1159
-
1160
-
1161
- @dataclass(kw_only=True, slots=True)
1162
- class ParseMonthError(Exception):
1163
- month: str
1164
-
1165
- @override
1166
- def __str__(self) -> str:
1167
- return f"Unable to parse month; got {self.month!r}"
1168
-
1169
-
1170
- ##
1171
-
1172
-
1173
- @overload
1174
- def sub_duration(
1175
- date: dt.datetime, /, *, duration: Duration | None = ...
1176
- ) -> dt.datetime: ...
1177
- @overload
1178
- def sub_duration(date: dt.date, /, *, duration: Duration | None = ...) -> dt.date: ...
1179
- def sub_duration(
1180
- date: DateOrDateTime, /, *, duration: Duration | None = None
1181
- ) -> dt.date:
1182
- """Subtract a duration from a date/datetime."""
1183
- if duration is None:
1184
- return date
1185
- try:
1186
- return add_duration(date, duration=-duration)
1187
- except AddDurationError:
1188
- raise SubDurationError(date=date, duration=duration) from None
1189
-
1190
-
1191
- @dataclass(kw_only=True, slots=True)
1192
- class SubDurationError(Exception):
1193
- date: dt.date
1194
- duration: Duration
1195
-
1196
- @override
1197
- def __str__(self) -> str:
1198
- return f"Date {self.date} must be paired with an integral duration; got {self.duration}"
1199
-
1200
-
1201
- ##
1202
-
1203
-
1204
- def timedelta_since_epoch(date_or_datetime: DateOrDateTime, /) -> dt.timedelta:
1205
- """Compute the timedelta since the epoch."""
1206
- match date_or_datetime:
1207
- case dt.datetime() as datetime:
1208
- if datetime.tzinfo is None:
1209
- return datetime - EPOCH_NAIVE
1210
- return datetime.astimezone(UTC) - EPOCH_UTC
1211
- case dt.date() as date:
1212
- return date - EPOCH_DATE
1213
- case _ as never:
1214
- assert_never(never)
1215
-
1216
-
1217
- ##
1218
-
1219
-
1220
- def yield_days(
1221
- *, start: dt.date | None = None, end: dt.date | None = None, days: int | None = None
1222
- ) -> Iterator[dt.date]:
1223
- """Yield the days in a range."""
1224
- match start, end, days:
1225
- case dt.date(), dt.date(), None:
1226
- check_date_not_datetime(start)
1227
- check_date_not_datetime(end)
1228
- date = start
1229
- while date <= end:
1230
- yield date
1231
- date += DAY
1232
- case dt.date(), None, int():
1233
- check_date_not_datetime(start)
1234
- date = start
1235
- for _ in range(days):
1236
- yield date
1237
- date += DAY
1238
- case None, dt.date(), int():
1239
- check_date_not_datetime(end)
1240
- date = end
1241
- for _ in range(days):
1242
- yield date
1243
- date -= DAY
1244
- case _:
1245
- raise YieldDaysError(start=start, end=end, days=days)
1246
-
1247
-
1248
- @dataclass(kw_only=True, slots=True)
1249
- class YieldDaysError(Exception):
1250
- start: dt.date | None
1251
- end: dt.date | None
1252
- days: int | None
1253
-
1254
- @override
1255
- def __str__(self) -> str:
1256
- return (
1257
- f"Invalid arguments: start={self.start}, end={self.end}, days={self.days}"
1258
- )
1259
-
1260
-
1261
- ##
1262
-
1263
-
1264
- def yield_weekdays(
1265
- *, start: dt.date | None = None, end: dt.date | None = None, days: int | None = None
1266
- ) -> Iterator[dt.date]:
1267
- """Yield the weekdays in a range."""
1268
- match start, end, days:
1269
- case dt.date(), dt.date(), None:
1270
- check_date_not_datetime(start)
1271
- check_date_not_datetime(end)
1272
- date = round_to_next_weekday(start)
1273
- while date <= end:
1274
- yield date
1275
- date = round_to_next_weekday(date + DAY)
1276
- case dt.date(), None, int():
1277
- check_date_not_datetime(start)
1278
- date = round_to_next_weekday(start)
1279
- for _ in range(days):
1280
- yield date
1281
- date = round_to_next_weekday(date + DAY)
1282
- case None, dt.date(), int():
1283
- check_date_not_datetime(end)
1284
- date = round_to_prev_weekday(end)
1285
- for _ in range(days):
1286
- yield date
1287
- date = round_to_prev_weekday(date - DAY)
1288
- case _:
1289
- raise YieldWeekdaysError(start=start, end=end, days=days)
1290
-
1291
-
1292
- @dataclass(kw_only=True, slots=True)
1293
- class YieldWeekdaysError(Exception):
1294
- start: dt.date | None
1295
- end: dt.date | None
1296
- days: int | None
1297
-
1298
- @override
1299
- def __str__(self) -> str:
1300
- return (
1301
- f"Invalid arguments: start={self.start}, end={self.end}, days={self.days}"
1302
- )
1303
-
1304
-
1305
- __all__ = [
1306
- "DATETIME_MAX_NAIVE",
1307
- "DATETIME_MAX_UTC",
1308
- "DATETIME_MIN_NAIVE",
1309
- "DATETIME_MIN_UTC",
1310
- "DAY",
1311
- "EPOCH_DATE",
1312
- "EPOCH_NAIVE",
1313
- "EPOCH_UTC",
1314
- "HALF_YEAR",
1315
- "HOUR",
1316
- "MAX_DATE_TWO_DIGIT_YEAR",
1317
- "MAX_MONTH",
1318
- "MILLISECOND",
1319
- "MINUTE",
1320
- "MIN_DATE_TWO_DIGIT_YEAR",
1321
- "MIN_MONTH",
1322
- "MONTH",
1323
- "NOW_UTC",
1324
- "QUARTER",
1325
- "SECOND",
1326
- "TODAY_UTC",
1327
- "WEEK",
1328
- "YEAR",
1329
- "ZERO_TIME",
1330
- "AddDurationError",
1331
- "AddWeekdaysError",
1332
- "AreEqualDateTimesError",
1333
- "AreEqualDatesOrDateTimesError",
1334
- "CheckDateNotDateTimeError",
1335
- "DateOrMonth",
1336
- "EnsureMonthError",
1337
- "GetMinMaxDateError",
1338
- "MeanDateTimeError",
1339
- "MeanTimeDeltaError",
1340
- "MillisecondsSinceEpochError",
1341
- "Month",
1342
- "MonthError",
1343
- "MonthLike",
1344
- "ParseDateCompactError",
1345
- "ParseDateTimeCompactError",
1346
- "ParseMonthError",
1347
- "SerializeCompactError",
1348
- "SubDurationError",
1349
- "TimedeltaToMillisecondsError",
1350
- "YieldDaysError",
1351
- "YieldWeekdaysError",
1352
- "add_duration",
1353
- "add_weekdays",
1354
- "are_equal_date_durations",
1355
- "are_equal_dates_or_datetimes",
1356
- "are_equal_datetime_durations",
1357
- "are_equal_datetimes",
1358
- "are_equal_months",
1359
- "check_date_not_datetime",
1360
- "date_duration_to_int",
1361
- "date_duration_to_timedelta",
1362
- "date_to_datetime",
1363
- "date_to_month",
1364
- "datetime_duration_to_float",
1365
- "datetime_duration_to_microseconds",
1366
- "datetime_duration_to_milliseconds",
1367
- "datetime_duration_to_timedelta",
1368
- "datetime_utc",
1369
- "days_since_epoch",
1370
- "days_since_epoch_to_date",
1371
- "ensure_month",
1372
- "format_datetime_local_and_utc",
1373
- "get_date",
1374
- "get_datetime",
1375
- "get_half_years",
1376
- "get_min_max_date",
1377
- "get_months",
1378
- "get_now",
1379
- "get_quarters",
1380
- "get_today",
1381
- "get_years",
1382
- "is_integral_timedelta",
1383
- "is_plain_datetime",
1384
- "is_weekday",
1385
- "is_zero_time",
1386
- "is_zoned_datetime",
1387
- "maybe_sub_pct_y",
1388
- "mean_datetime",
1389
- "mean_timedelta",
1390
- "microseconds_since_epoch",
1391
- "microseconds_since_epoch_to_datetime",
1392
- "microseconds_to_timedelta",
1393
- "milliseconds_since_epoch",
1394
- "milliseconds_since_epoch_to_datetime",
1395
- "milliseconds_to_timedelta",
1396
- "parse_date_compact",
1397
- "parse_datetime_compact",
1398
- "parse_month",
1399
- "parse_two_digit_year",
1400
- "round_datetime",
1401
- "round_to_next_weekday",
1402
- "round_to_prev_weekday",
1403
- "serialize_compact",
1404
- "serialize_month",
1405
- "sub_duration",
1406
- "timedelta_since_epoch",
1407
- "yield_days",
1408
- "yield_weekdays",
1409
- ]