dycw-utilities 0.131.15__py3-none-any.whl → 0.131.16__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.131.15
3
+ Version: 0.131.16
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,11 +1,11 @@
1
- utilities/__init__.py,sha256=TF72KRTntycdsGaFpyLS49_hW9qrELSmZAvg2kQ0BrA,61
1
+ utilities/__init__.py,sha256=I-o_EPHIejt3ggoklYx3Vx_Wi2XLiPZ3YlzjyX4JBKI,61
2
2
  utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
3
3
  utilities/altair.py,sha256=HeZBVUocjkrTNwwKrClppsIqgNFF-ykv05HfZSoHYno,9104
4
4
  utilities/asyncio.py,sha256=yfKvAIDCRrWdyQMVZMo4DJQx4nVrXoAcqwhNuF95Ryo,38186
5
5
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
6
6
  utilities/atools.py,sha256=-bFGIrwYMFR7xl39j02DZMsO_u5x5_Ph7bRlBUFVYyw,1048
7
7
  utilities/cachetools.py,sha256=uBtEv4hD-TuCPX_cQy1lOpLF-QqfwnYGSf0o4Soqydc,2826
8
- utilities/click.py,sha256=8gRYeyu9KQ3uim0UpC8VnFnOOKD3DyGwMJ7k0Qns1lM,14659
8
+ utilities/click.py,sha256=sdGAMoarAWJ4_A3o-UzM7hpe7R37yvC_X_Uh8vasa-A,13295
9
9
  utilities/concurrent.py,sha256=s2scTEd2AhXVTW4hpASU2qxV_DiVLALfms55cCQzCvM,2886
10
10
  utilities/contextlib.py,sha256=lpaLJBy3X0UGLWjM98jkQZZq8so4fRmoK-Bheq0uOW4,1027
11
11
  utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
@@ -24,7 +24,7 @@ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
24
24
  utilities/git.py,sha256=oi7-_l5e9haSANSCvQw25ufYGoNahuUPHAZ6114s3JQ,1191
25
25
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
26
26
  utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
27
- utilities/hypothesis.py,sha256=hEyuJoLH0c6So0P5OESvyANpfM49Ld1iYXC4bZvacm4,42603
27
+ utilities/hypothesis.py,sha256=fcklSUIagoDZOTn8ITbkIU7BOZMV4g1Owj0khiz9yBY,43401
28
28
  utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
29
29
  utilities/inflect.py,sha256=DbqB5Q9FbRGJ1NbvEiZBirRMxCxgrz91zy5jCO9ZIs0,347
30
30
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
@@ -88,11 +88,11 @@ utilities/tzlocal.py,sha256=xbBBzVIUKMk8AkhuIp1qxGRNBioIa5I09dpeoBnIOOU,662
88
88
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
89
89
  utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
90
90
  utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
91
- utilities/whenever.py,sha256=2NQ-0SnLNW2kFpefP9dVE8H0RbaeusXYLPmv282Jpto,16755
92
- utilities/whenever2.py,sha256=iFVL4CjuIOpzsDU6li5smHnDEqam30-FtTgXWeHuWiE,7510
91
+ utilities/whenever.py,sha256=oO0sgIIv4tvhIYmlZ4RFFfY6P54CPeyJCY9A4XHuyqo,11916
92
+ utilities/whenever2.py,sha256=Jd_fEavXCWTdMj29L7j-HbeMvrl8e3Ah_nxcLEFAwOU,12026
93
93
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
94
94
  utilities/zoneinfo.py,sha256=gJPr9l7V8s3Y7TXpCGYEM1S81Rplb9e4MoV9Nvy2VU8,1852
95
- dycw_utilities-0.131.15.dist-info/METADATA,sha256=Aztn9G7ACPHL_tGTOBayxzN6aPT_8gmKwsTUcieAoUA,1585
96
- dycw_utilities-0.131.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
- dycw_utilities-0.131.15.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
98
- dycw_utilities-0.131.15.dist-info/RECORD,,
95
+ dycw_utilities-0.131.16.dist-info/METADATA,sha256=X0Yt3BUVE7Y7W884VYvx_9CVhE4c9ENSxbhD4ghFoKU,1585
96
+ dycw_utilities-0.131.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
+ dycw_utilities-0.131.16.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
98
+ dycw_utilities-0.131.16.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.131.15"
3
+ __version__ = "0.131.16"
utilities/click.py CHANGED
@@ -1,40 +1,38 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime as dt
4
3
  import pathlib
5
- from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar, override
6
- from uuid import UUID
4
+ from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar, assert_never, override
7
5
 
8
6
  import click
7
+ import whenever
9
8
  from click import Choice, Context, Parameter, ParamType
10
- from click.types import (
11
- BoolParamType,
12
- FloatParamType,
13
- IntParamType,
14
- StringParamType,
15
- UUIDParameterType,
16
- )
9
+ from click.types import StringParamType
17
10
 
18
- import utilities.datetime
19
- import utilities.types
20
- from utilities.datetime import EnsureMonthError, MonthLike, ensure_month
11
+ from utilities.datetime import EnsureMonthError, ensure_month
21
12
  from utilities.enum import EnsureEnumError, ensure_enum
22
13
  from utilities.functions import EnsureStrError, ensure_str, get_class_name
23
14
  from utilities.iterables import is_iterable_not_str
24
15
  from utilities.text import split_str
25
16
  from utilities.types import (
26
- DateTimeLike,
17
+ DateDeltaLike,
18
+ DateLike,
19
+ DateTimeDeltaLike,
27
20
  EnumLike,
28
21
  MaybeStr,
29
- PyDateLike,
30
- PyTimeDeltaLike,
31
- PyTimeLike,
22
+ PlainDateTimeLike,
32
23
  TEnum,
24
+ TimeDeltaLike,
25
+ TimeLike,
26
+ ZonedDateTimeLike,
33
27
  )
34
28
 
35
29
  if TYPE_CHECKING:
36
30
  from collections.abc import Iterable, Sequence
37
31
 
32
+ import utilities.datetime
33
+ from utilities.datetime import MonthLike
34
+
35
+
38
36
  _T = TypeVar("_T")
39
37
  _TParam = TypeVar("_TParam", bound=ParamType)
40
38
 
@@ -76,21 +74,25 @@ class Date(ParamType):
76
74
 
77
75
  @override
78
76
  def convert(
79
- self, value: PyDateLike, param: Parameter | None, ctx: Context | None
80
- ) -> dt.date:
77
+ self, value: DateLike, param: Parameter | None, ctx: Context | None
78
+ ) -> whenever.Date:
81
79
  """Convert a value into the `Date` type."""
82
- from utilities.whenever import EnsureDateError, ensure_date
83
-
84
- try:
85
- return ensure_date(value)
86
- except EnsureDateError as error:
87
- self.fail(str(error), param, ctx)
80
+ match value:
81
+ case whenever.Date():
82
+ return value
83
+ case str():
84
+ try:
85
+ return whenever.Date.parse_common_iso(value)
86
+ except ValueError as error:
87
+ self.fail(str(error), param, ctx)
88
+ case _ as never:
89
+ assert_never(never)
88
90
 
89
91
 
90
- class Duration(ParamType):
91
- """A duration-valued parameter."""
92
+ class DateDelta(ParamType):
93
+ """A date-delta-valued parameter."""
92
94
 
93
- name = "duration"
95
+ name = "date delta"
94
96
 
95
97
  @override
96
98
  def __repr__(self) -> str:
@@ -98,18 +100,45 @@ class Duration(ParamType):
98
100
 
99
101
  @override
100
102
  def convert(
101
- self,
102
- value: MaybeStr[utilities.types.Duration],
103
- param: Parameter | None,
104
- ctx: Context | None,
105
- ) -> utilities.types.Duration:
106
- """Convert a value into the `Duration` type."""
107
- from utilities.whenever import EnsureDurationError, ensure_duration
103
+ self, value: DateDeltaLike, param: Parameter | None, ctx: Context | None
104
+ ) -> whenever.DateDelta:
105
+ """Convert a value into the `DateDelta` type."""
106
+ match value:
107
+ case whenever.DateDelta():
108
+ return value
109
+ case str():
110
+ try:
111
+ return whenever.DateDelta.parse_common_iso(value)
112
+ except ValueError as error:
113
+ self.fail(str(error), param, ctx)
114
+ case _ as never:
115
+ assert_never(never)
116
+
117
+
118
+ class DateTimeDelta(ParamType):
119
+ """A date-delta-valued parameter."""
120
+
121
+ name = "date-time delta"
108
122
 
109
- try:
110
- return ensure_duration(value)
111
- except EnsureDurationError as error:
112
- self.fail(str(error), param, ctx)
123
+ @override
124
+ def __repr__(self) -> str:
125
+ return self.name.upper()
126
+
127
+ @override
128
+ def convert(
129
+ self, value: DateTimeDeltaLike, param: Parameter | None, ctx: Context | None
130
+ ) -> whenever.DateTimeDelta:
131
+ """Convert a value into the `DateTimeDelta` type."""
132
+ match value:
133
+ case whenever.DateTimeDelta():
134
+ return value
135
+ case str():
136
+ try:
137
+ return whenever.DateTimeDelta.parse_common_iso(value)
138
+ except ValueError as error:
139
+ self.fail(str(error), param, ctx)
140
+ case _ as never:
141
+ assert_never(never)
113
142
 
114
143
 
115
144
  class Enum(ParamType, Generic[TEnum]):
@@ -167,7 +196,7 @@ class Month(ParamType):
167
196
  class PlainDateTime(ParamType):
168
197
  """A local-datetime-valued parameter."""
169
198
 
170
- name = "plain datetime"
199
+ name = "plain date-time"
171
200
 
172
201
  @override
173
202
  def __repr__(self) -> str:
@@ -175,15 +204,19 @@ class PlainDateTime(ParamType):
175
204
 
176
205
  @override
177
206
  def convert(
178
- self, value: DateTimeLike, param: Parameter | None, ctx: Context | None
179
- ) -> dt.date:
180
- """Convert a value into the `LocalDateTime` type."""
181
- from utilities.whenever import EnsurePlainDateTimeError, ensure_plain_datetime
182
-
183
- try:
184
- return ensure_plain_datetime(value)
185
- except EnsurePlainDateTimeError as error:
186
- self.fail(str(error), param, ctx)
207
+ self, value: PlainDateTimeLike, param: Parameter | None, ctx: Context | None
208
+ ) -> whenever.PlainDateTime:
209
+ """Convert a value into the `PlainDateTime` type."""
210
+ match value:
211
+ case whenever.PlainDateTime():
212
+ return value
213
+ case str():
214
+ try:
215
+ return whenever.PlainDateTime.parse_common_iso(value)
216
+ except ValueError as error:
217
+ self.fail(str(error), param, ctx)
218
+ case _ as never:
219
+ assert_never(never)
187
220
 
188
221
 
189
222
  class Time(ParamType):
@@ -197,21 +230,25 @@ class Time(ParamType):
197
230
 
198
231
  @override
199
232
  def convert(
200
- self, value: PyTimeLike, param: Parameter | None, ctx: Context | None
201
- ) -> dt.time:
233
+ self, value: TimeLike, param: Parameter | None, ctx: Context | None
234
+ ) -> whenever.Time:
202
235
  """Convert a value into the `Time` type."""
203
- from utilities.whenever import EnsureTimeError, ensure_time
204
-
205
- try:
206
- return ensure_time(value)
207
- except EnsureTimeError as error:
208
- return self.fail(str(error), param=param, ctx=ctx)
209
-
210
-
211
- class Timedelta(ParamType):
236
+ match value:
237
+ case whenever.Time():
238
+ return value
239
+ case str():
240
+ try:
241
+ return whenever.Time.parse_common_iso(value)
242
+ except ValueError as error:
243
+ self.fail(str(error), param, ctx)
244
+ case _ as never:
245
+ assert_never(never)
246
+
247
+
248
+ class TimeDelta(ParamType):
212
249
  """A timedelta-valued parameter."""
213
250
 
214
- name = "timedelta"
251
+ name = "time-delta"
215
252
 
216
253
  @override
217
254
  def __repr__(self) -> str:
@@ -219,21 +256,25 @@ class Timedelta(ParamType):
219
256
 
220
257
  @override
221
258
  def convert(
222
- self, value: PyTimeDeltaLike, param: Parameter | None, ctx: Context | None
223
- ) -> dt.timedelta:
224
- """Convert a value into the `Timedelta` type."""
225
- from utilities.whenever import EnsureTimedeltaError, ensure_timedelta
226
-
227
- try:
228
- return ensure_timedelta(value)
229
- except EnsureTimedeltaError as error:
230
- self.fail(str(error), param, ctx)
259
+ self, value: TimeDeltaLike, param: Parameter | None, ctx: Context | None
260
+ ) -> whenever.TimeDelta:
261
+ """Convert a value into the `TimeDelta` type."""
262
+ match value:
263
+ case whenever.TimeDelta():
264
+ return value
265
+ case str():
266
+ try:
267
+ return whenever.TimeDelta.parse_common_iso(value)
268
+ except ValueError as error:
269
+ self.fail(str(error), param, ctx)
270
+ case _ as never:
271
+ assert_never(never)
231
272
 
232
273
 
233
274
  class ZonedDateTime(ParamType):
234
275
  """A zoned-datetime-valued parameter."""
235
276
 
236
- name = "zoned datetime"
277
+ name = "zoned date-time"
237
278
 
238
279
  @override
239
280
  def __repr__(self) -> str:
@@ -241,15 +282,19 @@ class ZonedDateTime(ParamType):
241
282
 
242
283
  @override
243
284
  def convert(
244
- self, value: DateTimeLike, param: Parameter | None, ctx: Context | None
245
- ) -> dt.date:
246
- """Convert a value into the `DateTime` type."""
247
- from utilities.whenever import EnsureZonedDateTimeError, ensure_zoned_datetime
248
-
249
- try:
250
- return ensure_zoned_datetime(value)
251
- except EnsureZonedDateTimeError as error:
252
- self.fail(str(error), param, ctx)
285
+ self, value: ZonedDateTimeLike, param: Parameter | None, ctx: Context | None
286
+ ) -> whenever.ZonedDateTime:
287
+ """Convert a value into the `ZonedDateTime` type."""
288
+ match value:
289
+ case whenever.ZonedDateTime():
290
+ return value
291
+ case str():
292
+ try:
293
+ return whenever.ZonedDateTime.parse_common_iso(value)
294
+ except ValueError as error:
295
+ self.fail(str(error), param, ctx)
296
+ case _ as never:
297
+ assert_never(never)
253
298
 
254
299
 
255
300
  # parameters - frozenset
@@ -297,20 +342,6 @@ class FrozenSetParameter(ParamType, Generic[_TParam, _T]):
297
342
  return _make_metavar(param, desc)
298
343
 
299
344
 
300
- class FrozenSetBools(FrozenSetParameter[BoolParamType, str]):
301
- """A frozenset-of-bools-valued parameter."""
302
-
303
- def __init__(self, *, separator: str = ",") -> None:
304
- super().__init__(BoolParamType(), separator=separator)
305
-
306
-
307
- class FrozenSetDates(FrozenSetParameter[Date, dt.date]):
308
- """A frozenset-of-dates-valued parameter."""
309
-
310
- def __init__(self, *, separator: str = ",") -> None:
311
- super().__init__(Date(), separator=separator)
312
-
313
-
314
345
  class FrozenSetChoices(FrozenSetParameter[Choice, str]):
315
346
  """A frozenset-of-choices-valued parameter."""
316
347
 
@@ -341,27 +372,6 @@ class FrozenSetEnums(FrozenSetParameter[Enum[TEnum], TEnum]):
341
372
  super().__init__(Enum(enum, case_sensitive=case_sensitive), separator=separator)
342
373
 
343
374
 
344
- class FrozenSetFloats(FrozenSetParameter[FloatParamType, float]):
345
- """A frozenset-of-floats-valued parameter."""
346
-
347
- def __init__(self, *, separator: str = ",") -> None:
348
- super().__init__(FloatParamType(), separator=separator)
349
-
350
-
351
- class FrozenSetInts(FrozenSetParameter[IntParamType, int]):
352
- """A frozenset-of-ints-valued parameter."""
353
-
354
- def __init__(self, *, separator: str = ",") -> None:
355
- super().__init__(IntParamType(), separator=separator)
356
-
357
-
358
- class FrozenSetMonths(FrozenSetParameter[Month, utilities.datetime.Month]):
359
- """A frozenset-of-months-valued parameter."""
360
-
361
- def __init__(self, *, separator: str = ",") -> None:
362
- super().__init__(Month(), separator=separator)
363
-
364
-
365
375
  class FrozenSetStrs(FrozenSetParameter[StringParamType, str]):
366
376
  """A frozenset-of-strs-valued parameter."""
367
377
 
@@ -369,13 +379,6 @@ class FrozenSetStrs(FrozenSetParameter[StringParamType, str]):
369
379
  super().__init__(StringParamType(), separator=separator)
370
380
 
371
381
 
372
- class FrozenSetUUIDs(FrozenSetParameter[UUIDParameterType, UUID]):
373
- """A frozenset-of-UUIDs-valued parameter."""
374
-
375
- def __init__(self, *, separator: str = ",") -> None:
376
- super().__init__(UUIDParameterType(), separator=separator)
377
-
378
-
379
382
  # parameters - list
380
383
 
381
384
 
@@ -421,20 +424,6 @@ class ListParameter(ParamType, Generic[_TParam, _T]):
421
424
  return _make_metavar(param, desc)
422
425
 
423
426
 
424
- class ListBools(ListParameter[BoolParamType, str]):
425
- """A list-of-bools-valued parameter."""
426
-
427
- def __init__(self, *, separator: str = ",") -> None:
428
- super().__init__(BoolParamType(), separator=separator)
429
-
430
-
431
- class ListDates(ListParameter[Date, dt.date]):
432
- """A list-of-dates-valued parameter."""
433
-
434
- def __init__(self, *, separator: str = ",") -> None:
435
- super().__init__(Date(), separator=separator)
436
-
437
-
438
427
  class ListEnums(ListParameter[Enum[TEnum], TEnum]):
439
428
  """A list-of-enums-valued parameter."""
440
429
 
@@ -449,27 +438,6 @@ class ListEnums(ListParameter[Enum[TEnum], TEnum]):
449
438
  super().__init__(Enum(enum, case_sensitive=case_sensitive), separator=separator)
450
439
 
451
440
 
452
- class ListFloats(ListParameter[FloatParamType, float]):
453
- """A list-of-floats-valued parameter."""
454
-
455
- def __init__(self, *, separator: str = ",") -> None:
456
- super().__init__(FloatParamType(), separator=separator)
457
-
458
-
459
- class ListInts(ListParameter[IntParamType, int]):
460
- """A list-of-ints-valued parameter."""
461
-
462
- def __init__(self, *, separator: str = ",") -> None:
463
- super().__init__(IntParamType(), separator=separator)
464
-
465
-
466
- class ListMonths(ListParameter[Month, utilities.datetime.Month]):
467
- """A list-of-months-valued parameter."""
468
-
469
- def __init__(self, *, separator: str = ",") -> None:
470
- super().__init__(Month(), separator=separator)
471
-
472
-
473
441
  class ListStrs(ListParameter[StringParamType, str]):
474
442
  """A list-of-strs-valued parameter."""
475
443
 
@@ -477,13 +445,6 @@ class ListStrs(ListParameter[StringParamType, str]):
477
445
  super().__init__(StringParamType(), separator=separator)
478
446
 
479
447
 
480
- class ListUUIDs(ListParameter[UUIDParameterType, UUID]):
481
- """A list-of-UUIDs-valued parameter."""
482
-
483
- def __init__(self, *, separator: str = ",") -> None:
484
- super().__init__(UUIDParameterType(), separator=separator)
485
-
486
-
487
448
  # private
488
449
 
489
450
 
@@ -495,34 +456,22 @@ def _make_metavar(param: Parameter, desc: str, /) -> str:
495
456
  __all__ = [
496
457
  "CONTEXT_SETTINGS_HELP_OPTION_NAMES",
497
458
  "Date",
459
+ "DateDelta",
460
+ "DateTimeDelta",
498
461
  "DirPath",
499
- "Duration",
500
462
  "Enum",
501
463
  "ExistingDirPath",
502
464
  "ExistingFilePath",
503
465
  "FilePath",
504
- "FrozenSetBools",
505
466
  "FrozenSetChoices",
506
- "FrozenSetDates",
507
467
  "FrozenSetEnums",
508
- "FrozenSetFloats",
509
- "FrozenSetInts",
510
- "FrozenSetMonths",
511
468
  "FrozenSetParameter",
512
469
  "FrozenSetStrs",
513
- "FrozenSetUUIDs",
514
- "ListBools",
515
- "ListDates",
516
470
  "ListEnums",
517
- "ListFloats",
518
- "ListInts",
519
- "ListMonths",
520
471
  "ListParameter",
521
472
  "ListStrs",
522
- "ListUUIDs",
523
- "Month",
524
473
  "PlainDateTime",
525
474
  "Time",
526
- "Timedelta",
475
+ "TimeDelta",
527
476
  "ZonedDateTime",
528
477
  ]
utilities/hypothesis.py CHANGED
@@ -48,7 +48,15 @@ from hypothesis.strategies import (
48
48
  uuids,
49
49
  )
50
50
  from hypothesis.utils.conventions import not_set
51
- from whenever import Date, DateDelta, PlainDateTime, Time, TimeDelta, ZonedDateTime
51
+ from whenever import (
52
+ Date,
53
+ DateDelta,
54
+ DateTimeDelta,
55
+ PlainDateTime,
56
+ Time,
57
+ TimeDelta,
58
+ ZonedDateTime,
59
+ )
52
60
 
53
61
  from utilities.datetime import (
54
62
  DATETIME_MAX_NAIVE,
@@ -97,12 +105,19 @@ from utilities.whenever2 import (
97
105
  DATE_DELTA_PARSABLE_MIN,
98
106
  DATE_MAX,
99
107
  DATE_MIN,
108
+ DATE_TIME_DELTA_MAX,
109
+ DATE_TIME_DELTA_MIN,
110
+ DATE_TIME_DELTA_PARSABLE_MAX,
111
+ DATE_TIME_DELTA_PARSABLE_MIN,
100
112
  PLAIN_DATE_TIME_MAX,
101
113
  PLAIN_DATE_TIME_MIN,
102
114
  TIME_DELTA_MAX,
103
115
  TIME_DELTA_MIN,
104
116
  TIME_MAX,
105
117
  TIME_MIN,
118
+ to_date_time_delta,
119
+ to_days,
120
+ to_nanos,
106
121
  )
107
122
  from utilities.zoneinfo import UTC, ensure_time_zone
108
123
 
@@ -199,25 +214,11 @@ def date_deltas_whenever(
199
214
  ...
200
215
  case _ as never:
201
216
  assert_never(never)
202
- min_years, min_months, min_days = min_value_.in_years_months_days()
203
- assert min_years == 0
204
- assert min_months == 0
205
- max_years, max_months, max_days = max_value_.in_years_months_days()
206
- assert max_years == 0
207
- assert max_months == 0
217
+ min_days = to_days(min_value_)
218
+ max_days = to_days(max_value_)
208
219
  if draw2(draw, parsable):
209
- parsable_min_years, parsable_min_months, parsable_min_days = (
210
- DATE_DELTA_PARSABLE_MIN.in_years_months_days()
211
- )
212
- assert parsable_min_years == 0
213
- assert parsable_min_months == 0
214
- min_days = max(min_days, parsable_min_days)
215
- parsable_max_years, parsable_max_months, parsable_max_days = (
216
- DATE_DELTA_PARSABLE_MAX.in_years_months_days()
217
- )
218
- assert parsable_max_years == 0
219
- assert parsable_max_months == 0
220
- max_days = min(max_days, parsable_max_days)
220
+ min_days = max(min_days, to_days(DATE_DELTA_PARSABLE_MIN))
221
+ max_days = min(max_days, to_days(DATE_DELTA_PARSABLE_MAX))
221
222
  days = draw(integers(min_value=min_days, max_value=max_days))
222
223
  return DateDelta(days=days)
223
224
 
@@ -287,6 +288,42 @@ def _is_between_timedelta(
287
288
  ##
288
289
 
289
290
 
291
+ @composite
292
+ def date_time_deltas_whenever(
293
+ draw: DrawFn,
294
+ /,
295
+ *,
296
+ min_value: MaybeSearchStrategy[DateTimeDelta | None] = None,
297
+ max_value: MaybeSearchStrategy[DateTimeDelta | None] = None,
298
+ parsable: MaybeSearchStrategy[bool] = False,
299
+ ) -> DateTimeDelta:
300
+ """Strategy for generating date deltas."""
301
+ min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
302
+ match min_value_:
303
+ case None:
304
+ min_value_ = DATE_TIME_DELTA_MIN
305
+ case DateTimeDelta():
306
+ ...
307
+ case _ as never:
308
+ assert_never(never)
309
+ match max_value_:
310
+ case None:
311
+ max_value_ = DATE_TIME_DELTA_MAX
312
+ case DateTimeDelta():
313
+ ...
314
+ case _ as never:
315
+ assert_never(never)
316
+ min_nanos, max_nanos = map(to_nanos, [min_value_, max_value_])
317
+ if draw2(draw, parsable):
318
+ min_nanos = max(min_nanos, to_nanos(DATE_TIME_DELTA_PARSABLE_MIN))
319
+ max_nanos = min(max_nanos, to_nanos(DATE_TIME_DELTA_PARSABLE_MAX))
320
+ nanos = draw(integers(min_value=min_nanos, max_value=max_nanos))
321
+ return to_date_time_delta(nanos)
322
+
323
+
324
+ ##
325
+
326
+
290
327
  @composite
291
328
  def dates_two_digit_year(
292
329
  draw: DrawFn,
@@ -1468,6 +1505,7 @@ __all__ = [
1468
1505
  "bool_arrays",
1469
1506
  "date_deltas_whenever",
1470
1507
  "date_durations",
1508
+ "date_time_deltas_whenever",
1471
1509
  "dates_two_digit_year",
1472
1510
  "dates_whenever",
1473
1511
  "datetime_durations",
utilities/whenever.py CHANGED
@@ -33,14 +33,7 @@ from utilities.re import (
33
33
  from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
34
34
 
35
35
  if TYPE_CHECKING:
36
- from utilities.types import (
37
- DateTimeLike,
38
- Duration,
39
- DurationLike,
40
- PyDateLike,
41
- PyTimeDeltaLike,
42
- PyTimeLike,
43
- )
36
+ from utilities.types import Duration
44
37
 
45
38
 
46
39
  MAX_SERIALIZABLE_TIMEDELTA = dt.timedelta(days=3652060, microseconds=-1)
@@ -91,177 +84,6 @@ class _CheckValidZonedDateTimeUnequalError(CheckValidZonedDateTimeError):
91
84
  ##
92
85
 
93
86
 
94
- def ensure_date(date: PyDateLike, /) -> dt.date:
95
- """Ensure the object is a date."""
96
- if isinstance(date, dt.date):
97
- check_date_not_datetime(date)
98
- return date
99
- try:
100
- return parse_date(date)
101
- except ParseDateError as error:
102
- raise EnsureDateError(date=error.date) from None
103
-
104
-
105
- @dataclass(kw_only=True, slots=True)
106
- class EnsureDateError(Exception):
107
- date: str
108
-
109
- @override
110
- def __str__(self) -> str:
111
- return f"Unable to ensure date; got {self.date!r}"
112
-
113
-
114
- ##
115
-
116
-
117
- def ensure_datetime(datetime: DateTimeLike, /) -> dt.datetime:
118
- """Ensure the object is a datetime."""
119
- if isinstance(datetime, dt.datetime):
120
- return datetime # skipif-ci-and-windows
121
- try:
122
- return parse_datetime(datetime)
123
- except ParseDateTimeError as error:
124
- raise EnsureDateTimeError(datetime=error.datetime) from None
125
-
126
-
127
- @dataclass(kw_only=True, slots=True)
128
- class EnsureDateTimeError(Exception):
129
- datetime: str
130
-
131
- @override
132
- def __str__(self) -> str:
133
- return f"Unable to ensure datetime; got {self.datetime!r}"
134
-
135
-
136
- ##
137
-
138
-
139
- def ensure_duration(duration: DurationLike, /) -> Duration:
140
- """Ensure the object is a Duration."""
141
- if isinstance(duration, int | float | dt.timedelta):
142
- return duration
143
- try:
144
- return parse_duration(duration)
145
- except ParseDurationError as error:
146
- raise EnsureDurationError(duration=error.duration) from None
147
-
148
-
149
- @dataclass(kw_only=True, slots=True)
150
- class EnsureDurationError(Exception):
151
- duration: str
152
-
153
- @override
154
- def __str__(self) -> str:
155
- return f"Unable to ensure duration; got {self.duration!r}"
156
-
157
-
158
- ##
159
-
160
-
161
- def ensure_plain_datetime(datetime: DateTimeLike, /) -> dt.datetime:
162
- """Ensure the object is a plain datetime."""
163
- if isinstance(datetime, dt.datetime):
164
- return datetime
165
- try:
166
- return parse_plain_datetime(datetime)
167
- except ParsePlainDateTimeError as error:
168
- raise EnsurePlainDateTimeError(datetime=error.datetime) from None
169
-
170
-
171
- @dataclass(kw_only=True, slots=True)
172
- class EnsurePlainDateTimeError(Exception):
173
- datetime: str
174
-
175
- @override
176
- def __str__(self) -> str:
177
- return f"Unable to ensure plain datetime; got {self.datetime!r}"
178
-
179
-
180
- ##
181
-
182
-
183
- def ensure_time(time: PyTimeLike, /) -> dt.time:
184
- """Ensure the object is a time."""
185
- if isinstance(time, dt.time):
186
- return time
187
- try:
188
- return parse_time(time)
189
- except ParseTimeError as error:
190
- raise EnsureTimeError(time=error.time) from None
191
-
192
-
193
- @dataclass(kw_only=True, slots=True)
194
- class EnsureTimeError(Exception):
195
- time: str
196
-
197
- @override
198
- def __str__(self) -> str:
199
- return f"Unable to ensure time; got {self.time!r}"
200
-
201
-
202
- ##
203
-
204
-
205
- def ensure_timedelta(timedelta: PyTimeDeltaLike, /) -> dt.timedelta:
206
- """Ensure the object is a timedelta."""
207
- if isinstance(timedelta, dt.timedelta):
208
- return timedelta
209
- try:
210
- return parse_timedelta(timedelta)
211
- except _ParseTimedeltaParseError as error:
212
- raise _EnsureTimedeltaParseError(timedelta=error.timedelta) from None
213
- except _ParseTimedeltaNanosecondError as error:
214
- raise _EnsureTimedeltaNanosecondError(
215
- timedelta=error.timedelta, nanoseconds=error.nanoseconds
216
- ) from None
217
-
218
-
219
- @dataclass(kw_only=True, slots=True)
220
- class EnsureTimedeltaError(Exception):
221
- timedelta: str
222
-
223
-
224
- @dataclass(kw_only=True, slots=True)
225
- class _EnsureTimedeltaParseError(EnsureTimedeltaError):
226
- @override
227
- def __str__(self) -> str:
228
- return f"Unable to ensure timedelta; got {self.timedelta!r}"
229
-
230
-
231
- @dataclass(kw_only=True, slots=True)
232
- class _EnsureTimedeltaNanosecondError(EnsureTimedeltaError):
233
- nanoseconds: int
234
-
235
- @override
236
- def __str__(self) -> str:
237
- return f"Unable to ensure timedelta; got {self.nanoseconds} nanoseconds"
238
-
239
-
240
- ##
241
-
242
-
243
- def ensure_zoned_datetime(datetime: DateTimeLike, /) -> dt.datetime:
244
- """Ensure the object is a zoned datetime."""
245
- if isinstance(datetime, dt.datetime):
246
- return datetime
247
- try:
248
- return parse_zoned_datetime(datetime)
249
- except ParseZonedDateTimeError as error:
250
- raise EnsureZonedDateTimeError(datetime=error.datetime) from None
251
-
252
-
253
- @dataclass(kw_only=True, slots=True)
254
- class EnsureZonedDateTimeError(Exception):
255
- datetime: str
256
-
257
- @override
258
- def __str__(self) -> str:
259
- return f"Unable to ensure zoned datetime; got {self.datetime!r}"
260
-
261
-
262
- ##
263
-
264
-
265
87
  _PARSE_DATE_YYMMDD_REGEX = re.compile(r"^(\d{2})(\d{2})(\d{2})$")
266
88
 
267
89
 
@@ -593,12 +415,6 @@ __all__ = [
593
415
  "MAX_SERIALIZABLE_TIMEDELTA",
594
416
  "MIN_SERIALIZABLE_TIMEDELTA",
595
417
  "CheckValidZonedDateTimeError",
596
- "EnsureDateError",
597
- "EnsureDateTimeError",
598
- "EnsurePlainDateTimeError",
599
- "EnsureTimeError",
600
- "EnsureTimedeltaError",
601
- "EnsureZonedDateTimeError",
602
418
  "ParseDateError",
603
419
  "ParseDateTimeError",
604
420
  "ParseDurationError",
@@ -611,13 +427,6 @@ __all__ = [
611
427
  "SerializeTimeDeltaError",
612
428
  "SerializeZonedDateTimeError",
613
429
  "check_valid_zoned_datetime",
614
- "ensure_date",
615
- "ensure_datetime",
616
- "ensure_duration",
617
- "ensure_plain_datetime",
618
- "ensure_time",
619
- "ensure_timedelta",
620
- "ensure_zoned_datetime",
621
430
  "parse_date",
622
431
  "parse_datetime",
623
432
  "parse_duration",
utilities/whenever2.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
4
  from collections.abc import Callable
5
+ from dataclasses import dataclass
5
6
  from functools import cache
6
7
  from logging import LogRecord
7
8
  from typing import TYPE_CHECKING, Any, assert_never, overload, override
@@ -17,6 +18,7 @@ from whenever import (
17
18
  )
18
19
 
19
20
  from utilities.datetime import maybe_sub_pct_y
21
+ from utilities.math import sign
20
22
  from utilities.sentinel import Sentinel, sentinel
21
23
  from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
22
24
  from utilities.zoneinfo import UTC, get_time_zone_name
@@ -44,16 +46,52 @@ PLAIN_DATE_TIME_MIN = PlainDateTime.from_py_datetime(dt.datetime.min) # noqa: D
44
46
  PLAIN_DATE_TIME_MAX = PlainDateTime.from_py_datetime(dt.datetime.max) # noqa: DTZ901
45
47
  ZONED_DATE_TIME_MIN = PLAIN_DATE_TIME_MIN.assume_tz(UTC.key)
46
48
  ZONED_DATE_TIME_MAX = PLAIN_DATE_TIME_MAX.assume_tz(UTC.key)
47
- DATE_TIME_DELTA_MIN = DateTimeDelta(days=-3652059, seconds=-316192377600)
48
- DATE_TIME_DELTA_MAX = DateTimeDelta(days=3652059, seconds=316192377600)
49
- DATE_DELTA_MIN = DATE_TIME_DELTA_MIN.date_part()
50
- DATE_DELTA_MAX = DATE_TIME_DELTA_MAX.date_part()
51
- TIME_DELTA_MIN = DATE_TIME_DELTA_MIN.time_part()
52
- TIME_DELTA_MAX = DATE_TIME_DELTA_MAX.time_part()
53
49
 
54
50
 
55
- DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta(days=-999999, seconds=-316192377600)
56
- DATE_TIME_DELTA_PARSABLE_MAX = DateTimeDelta(days=999999, seconds=316192377600)
51
+ DATE_TIME_DELTA_MIN = DateTimeDelta(
52
+ weeks=-521722,
53
+ days=-5,
54
+ hours=-23,
55
+ minutes=-59,
56
+ seconds=-59,
57
+ milliseconds=-999,
58
+ microseconds=-999,
59
+ nanoseconds=-999,
60
+ )
61
+ DATE_TIME_DELTA_MAX = DateTimeDelta(
62
+ weeks=521722,
63
+ days=5,
64
+ hours=23,
65
+ minutes=59,
66
+ seconds=59,
67
+ milliseconds=999,
68
+ microseconds=999,
69
+ nanoseconds=999,
70
+ )
71
+ DATE_DELTA_MIN = DATE_TIME_DELTA_MIN.date_part()
72
+ DATE_DELTA_MAX = DATE_TIME_DELTA_MAX.date_part()
73
+ TIME_DELTA_MIN = TimeDelta(hours=-87831216)
74
+ TIME_DELTA_MAX = TimeDelta(hours=87831216)
75
+
76
+
77
+ DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta(
78
+ weeks=-142857,
79
+ hours=-23,
80
+ minutes=-59,
81
+ seconds=-59,
82
+ milliseconds=-999,
83
+ microseconds=-999,
84
+ nanoseconds=-999,
85
+ )
86
+ DATE_TIME_DELTA_PARSABLE_MAX = DateTimeDelta(
87
+ weeks=142857,
88
+ hours=23,
89
+ minutes=59,
90
+ seconds=59,
91
+ milliseconds=999,
92
+ microseconds=999,
93
+ nanoseconds=999,
94
+ )
57
95
  DATE_DELTA_PARSABLE_MIN = DateDelta(days=-999999)
58
96
  DATE_DELTA_PARSABLE_MAX = DateDelta(days=999999)
59
97
 
@@ -163,6 +201,164 @@ def to_date(
163
201
  assert_never(never)
164
202
 
165
203
 
204
+ ##
205
+
206
+
207
+ def to_days(delta: DateDelta, /) -> int:
208
+ """Compute the number of days in a date delta."""
209
+ months, days = delta.in_months_days()
210
+ if months != 0:
211
+ raise ToDaysError(months=months)
212
+ return days
213
+
214
+
215
+ @dataclass(kw_only=True, slots=True)
216
+ class ToDaysError(Exception):
217
+ months: int
218
+
219
+ @override
220
+ def __str__(self) -> str:
221
+ return f"Date delta must not contain months; got {self.months}"
222
+
223
+
224
+ ##
225
+
226
+
227
+ def to_date_time_delta(nanos: int, /) -> DateTimeDelta:
228
+ """Construct a date-time delta."""
229
+ components = _to_time_delta_components(nanos)
230
+ days, hours = divmod(components.hours, 24)
231
+ weeks, days = divmod(days, 7)
232
+ match sign(nanos): # pragma: no cover
233
+ case 1:
234
+ if hours < 0:
235
+ hours += 24
236
+ days -= 1
237
+ if days < 0:
238
+ days += 7
239
+ weeks -= 1
240
+ case -1:
241
+ if hours > 0:
242
+ hours -= 24
243
+ days += 1
244
+ if days > 0:
245
+ days -= 7
246
+ weeks += 1
247
+ case 0:
248
+ ...
249
+ return DateTimeDelta(
250
+ weeks=weeks,
251
+ days=days,
252
+ hours=hours,
253
+ minutes=components.minutes,
254
+ seconds=components.seconds,
255
+ microseconds=components.microseconds,
256
+ milliseconds=components.milliseconds,
257
+ nanoseconds=components.nanoseconds,
258
+ )
259
+
260
+
261
+ ##
262
+
263
+
264
+ def to_nanos(delta: DateTimeDelta, /) -> int:
265
+ """Compute the number of nanoseconds in a date-time delta."""
266
+ months, days, _, _ = delta.in_months_days_secs_nanos()
267
+ if months != 0:
268
+ raise ToNanosError(months=months)
269
+ return 24 * 60 * 60 * int(1e9) * days + delta.time_part().in_nanoseconds()
270
+
271
+
272
+ @dataclass(kw_only=True, slots=True)
273
+ class ToNanosError(Exception):
274
+ months: int
275
+
276
+ @override
277
+ def __str__(self) -> str:
278
+ return f"Date-time delta must not contain months; got {self.months}"
279
+
280
+
281
+ ##
282
+
283
+
284
+ def to_time_delta(nanos: int, /) -> TimeDelta:
285
+ """Construct a time delta."""
286
+ components = _to_time_delta_components(nanos)
287
+ return TimeDelta(
288
+ hours=components.hours,
289
+ minutes=components.minutes,
290
+ seconds=components.seconds,
291
+ microseconds=components.microseconds,
292
+ milliseconds=components.milliseconds,
293
+ nanoseconds=components.nanoseconds,
294
+ )
295
+
296
+
297
+ @dataclass(kw_only=True, slots=True)
298
+ class _TimeDeltaComponents:
299
+ hours: int
300
+ minutes: int
301
+ seconds: int
302
+ microseconds: int
303
+ milliseconds: int
304
+ nanoseconds: int
305
+
306
+
307
+ def _to_time_delta_components(nanos: int, /) -> _TimeDeltaComponents:
308
+ sign_use = sign(nanos)
309
+ micros, nanos = divmod(nanos, int(1e3))
310
+ millis, micros = divmod(micros, int(1e3))
311
+ secs, millis = divmod(millis, int(1e3))
312
+ mins, secs = divmod(secs, 60)
313
+ hours, mins = divmod(mins, 60)
314
+ match sign_use: # pragma: no cover
315
+ case 1:
316
+ if nanos < 0:
317
+ nanos += int(1e3)
318
+ micros -= 1
319
+ if micros < 0:
320
+ micros += int(1e3)
321
+ millis -= 1
322
+ if millis < 0:
323
+ millis += int(1e3)
324
+ secs -= 1
325
+ if secs < 0:
326
+ secs += 60
327
+ mins -= 1
328
+ if mins < 0:
329
+ mins += 60
330
+ hours -= 1
331
+ case -1:
332
+ if nanos > 0:
333
+ nanos -= int(1e3)
334
+ micros += 1
335
+ if micros > 0:
336
+ micros -= int(1e3)
337
+ millis += 1
338
+ if millis > 0:
339
+ millis -= int(1e3)
340
+ secs += 1
341
+ if secs > 0:
342
+ secs -= 60
343
+ mins += 1
344
+ if mins > 0:
345
+ mins -= 60
346
+ hours += 1
347
+ case 0:
348
+ ...
349
+ return _TimeDeltaComponents(
350
+ hours=hours,
351
+ minutes=mins,
352
+ seconds=secs,
353
+ microseconds=micros,
354
+ milliseconds=millis,
355
+ nanoseconds=nanos,
356
+ )
357
+
358
+
359
+ ##
360
+
361
+
166
362
  @overload
167
363
  def to_zoned_date_time(*, date_time: MaybeCallableZonedDateTime) -> ZonedDateTime: ...
168
364
  @overload
@@ -265,6 +461,8 @@ __all__ = [
265
461
  "ZERO_TIME",
266
462
  "ZONED_DATE_TIME_MAX",
267
463
  "ZONED_DATE_TIME_MIN",
464
+ "ToDaysError",
465
+ "ToNanosError",
268
466
  "WheneverLogRecord",
269
467
  "format_compact",
270
468
  "format_compact",
@@ -276,5 +474,8 @@ __all__ = [
276
474
  "get_today",
277
475
  "get_today_local",
278
476
  "to_date",
477
+ "to_date_time_delta",
478
+ "to_days",
479
+ "to_nanos",
279
480
  "to_zoned_date_time",
280
481
  ]