dycw-utilities 0.133.0__py3-none-any.whl → 0.133.1__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.133.0
3
+ Version: 0.133.1
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=YfKvUqWoneovoyLwbO-FdwUu0a33vS1nujwBMcawq9Y,60
1
+ utilities/__init__.py,sha256=Z40RN6IDVR495cIDu4FY4lG1K8hcbWIcPkM_PM_rASQ,60
2
2
  utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
3
3
  utilities/altair.py,sha256=HeZBVUocjkrTNwwKrClppsIqgNFF-ykv05HfZSoHYno,9104
4
4
  utilities/arq.py,sha256=YkwvWoL930hgeU9VP8iuP3RhMf0t8sm7O8qsD9TiyWo,4688
@@ -6,7 +6,7 @@ utilities/asyncio.py,sha256=USWMMrHqPVRr20vlIn_n5JLimyqa-5xLhuqDYWJed8A,37586
6
6
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
7
7
  utilities/atools.py,sha256=-bFGIrwYMFR7xl39j02DZMsO_u5x5_Ph7bRlBUFVYyw,1048
8
8
  utilities/cachetools.py,sha256=uBtEv4hD-TuCPX_cQy1lOpLF-QqfwnYGSf0o4Soqydc,2826
9
- utilities/click.py,sha256=DI8yJFlpBpRvnc90Xc0kfLKGQRpFCvj797oOJiaE4k8,14998
9
+ utilities/click.py,sha256=2k7Ss2qKwYb1JCDB5IWpNf-B2WTyjKR1GxDW-Y6anPs,15736
10
10
  utilities/concurrent.py,sha256=s2scTEd2AhXVTW4hpASU2qxV_DiVLALfms55cCQzCvM,2886
11
11
  utilities/contextlib.py,sha256=lpaLJBy3X0UGLWjM98jkQZZq8so4fRmoK-Bheq0uOW4,1027
12
12
  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=MS0UgZjevC9QuJAUlGa8ozcbAhlq1qZnSRiSk_1KsXg,35204
27
+ utilities/hypothesis.py,sha256=LWEcjL0ip0cwawcIHhnWPVnB1OEH1GJnYEwYU6xtFpo,36259
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
@@ -78,7 +78,7 @@ utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
78
78
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
79
79
  utilities/timer.py,sha256=oYqRQ-G-DMOOHB6a4yP5-PJDVimLnbNkMnkOj_jUmFg,2474
80
80
  utilities/traceback.py,sha256=i-790AQbTrDA8MiYyOcYPFpm48I558VR_kL_7x4ypfY,8503
81
- utilities/typed_settings.py,sha256=DqJsJjSit9wd_OA3KyMDpx2zatKIi5QhuARI9TPl3rk,3701
81
+ utilities/typed_settings.py,sha256=io3bhnglxO5FRNuTz1vpgbbGgvyj6VGJ5pytPRUeJo4,3769
82
82
  utilities/types.py,sha256=ZvD16TobtB47IgMo2CK_CCdJsvhrTqAZgqgqbCME2T0,19223
83
83
  utilities/typing.py,sha256=kVWK6ciV8T0MKxnFQcMSEr_XlRisspH5aBTTosMUh30,13872
84
84
  utilities/tzdata.py,sha256=fgNVj66yUbCSI_-vrRVzSD3gtf-L_8IEJEPjP_Jel5Y,266
@@ -86,10 +86,10 @@ utilities/tzlocal.py,sha256=KyCXEgCTjqGFx-389JdTuhMRUaT06U1RCMdWoED-qro,728
86
86
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
87
87
  utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
88
88
  utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
89
- utilities/whenever.py,sha256=tArX9unVEKhRYdvbUFa83e4hrzdtMKKCEN4QWTaYd8c,19524
89
+ utilities/whenever.py,sha256=A-yoOqBqrcVD1yDINDsTFDw7dq9-zgUGn_f8CxVUQJs,23332
90
90
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
91
91
  utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
92
- dycw_utilities-0.133.0.dist-info/METADATA,sha256=8N2dkPiAUTKwY281OkaOGdvSP52CcijVTnjX9L63ctY,1522
93
- dycw_utilities-0.133.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- dycw_utilities-0.133.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
- dycw_utilities-0.133.0.dist-info/RECORD,,
92
+ dycw_utilities-0.133.1.dist-info/METADATA,sha256=fZj3XXeQfb7y2obavSG6zSthc-oG_Zt-I33T3RVsHOE,1522
93
+ dycw_utilities-0.133.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ dycw_utilities-0.133.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.133.1.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.133.0"
3
+ __version__ = "0.133.1"
utilities/click.py CHANGED
@@ -29,7 +29,7 @@ from utilities.types import (
29
29
  TimeLike,
30
30
  ZonedDateTimeLike,
31
31
  )
32
- from utilities.whenever import _MonthParseCommonISOError
32
+ from utilities.whenever import FreqLike, _FreqParseError, _MonthParseCommonISOError
33
33
 
34
34
  if TYPE_CHECKING:
35
35
  from collections.abc import Iterable, Sequence
@@ -177,6 +177,30 @@ class Enum(ParamType, Generic[TEnum]):
177
177
  return _make_metavar(param, desc)
178
178
 
179
179
 
180
+ class Freq(ParamType):
181
+ """An frequency-valued parameter."""
182
+
183
+ @override
184
+ def __repr__(self) -> str:
185
+ return "FREQ"
186
+
187
+ @override
188
+ def convert(
189
+ self, value: FreqLike, param: Parameter | None, ctx: Context | None
190
+ ) -> utilities.whenever.Freq:
191
+ """Convert a value into the `Freq` type."""
192
+ match value:
193
+ case utilities.whenever.Freq():
194
+ return value
195
+ case str():
196
+ try:
197
+ return utilities.whenever.Freq.parse(value)
198
+ except _FreqParseError as error:
199
+ self.fail(str(error), param, ctx)
200
+ case _ as never:
201
+ assert_never(never)
202
+
203
+
180
204
  class IPv4Address(ParamType):
181
205
  """An IPv4 address-valued parameter."""
182
206
 
@@ -519,6 +543,7 @@ __all__ = [
519
543
  "ExistingDirPath",
520
544
  "ExistingFilePath",
521
545
  "FilePath",
546
+ "Freq",
522
547
  "FrozenSetChoices",
523
548
  "FrozenSetEnums",
524
549
  "FrozenSetParameter",
utilities/hypothesis.py CHANGED
@@ -77,6 +77,8 @@ from utilities.pathlib import temp_cwd
77
77
  from utilities.platform import IS_WINDOWS
78
78
  from utilities.sentinel import Sentinel, sentinel
79
79
  from utilities.tempfile import TEMP_DIR, TemporaryDirectory
80
+ from utilities.types import DateTimeRoundUnit
81
+ from utilities.typing import get_literal_elements
80
82
  from utilities.version import Version
81
83
  from utilities.whenever import (
82
84
  DATE_DELTA_MAX,
@@ -100,6 +102,7 @@ from utilities.whenever import (
100
102
  TIME_DELTA_MIN,
101
103
  TIME_MAX,
102
104
  TIME_MIN,
105
+ Freq,
103
106
  Month,
104
107
  to_date_time_delta,
105
108
  to_days,
@@ -502,6 +505,38 @@ def floats_extra(
502
505
  ##
503
506
 
504
507
 
508
+ @composite
509
+ def freqs(
510
+ draw: DrawFn, /, *, unit: MaybeSearchStrategy[DateTimeRoundUnit | None] = None
511
+ ) -> Freq:
512
+ unit_ = draw2(draw, unit, _freq_units())
513
+ match unit_:
514
+ case "day":
515
+ return Freq(unit=unit_)
516
+ case "hour":
517
+ return Freq(unit=unit_, increment=draw(_freq_increments(24)))
518
+ case "minute" | "second":
519
+ return Freq(unit=unit_, increment=draw(_freq_increments(60)))
520
+ case "millisecond" | "microsecond" | "nanosecond":
521
+ return Freq(unit=unit_, increment=draw(_freq_increments(1000)))
522
+ case _ as never:
523
+ assert_never(never)
524
+
525
+
526
+ @composite
527
+ def _freq_units(draw: DrawFn, /) -> DateTimeRoundUnit:
528
+ return draw(sampled_from(get_literal_elements(DateTimeRoundUnit)))
529
+
530
+
531
+ @composite
532
+ def _freq_increments(draw: DrawFn, n: int, /) -> int:
533
+ divisors = [i for i in range(1, n) if n % i == 0]
534
+ return draw(sampled_from(divisors))
535
+
536
+
537
+ ##
538
+
539
+
505
540
  @composite
506
541
  def git_repos(draw: DrawFn, /) -> Path:
507
542
  path = draw(temp_paths())
@@ -1264,6 +1299,7 @@ __all__ = [
1264
1299
  "float64s",
1265
1300
  "float_arrays",
1266
1301
  "floats_extra",
1302
+ "freqs",
1267
1303
  "git_repos",
1268
1304
  "hashables",
1269
1305
  "int32s",
@@ -21,6 +21,7 @@ from whenever import (
21
21
  )
22
22
 
23
23
  from utilities.iterables import always_iterable
24
+ from utilities.whenever import Freq
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from collections.abc import Callable
@@ -52,6 +53,7 @@ class ExtendedTSConverter(TSConverter):
52
53
  (Date, Date.parse_common_iso),
53
54
  (DateDelta, DateDelta.parse_common_iso),
54
55
  (DateTimeDelta, DateTimeDelta.parse_common_iso),
56
+ (Freq, Freq.parse),
55
57
  (IPv4Address, IPv4Address),
56
58
  (IPv6Address, IPv6Address),
57
59
  (PlainDateTime, PlainDateTime.parse_common_iso),
utilities/whenever.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable, Iterable
3
+ from collections.abc import Callable, Iterable, Mapping
4
4
  from dataclasses import dataclass, replace
5
5
  from functools import cache
6
6
  from logging import LogRecord
@@ -8,9 +8,12 @@ from statistics import fmean
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
10
  Any,
11
+ ClassVar,
12
+ Literal,
11
13
  Self,
12
14
  SupportsFloat,
13
15
  assert_never,
16
+ cast,
14
17
  overload,
15
18
  override,
16
19
  )
@@ -28,7 +31,7 @@ from utilities.math import sign
28
31
  from utilities.platform import get_strftime
29
32
  from utilities.re import ExtractGroupsError, extract_groups
30
33
  from utilities.sentinel import Sentinel, sentinel
31
- from utilities.types import MaybeStr
34
+ from utilities.types import DateTimeRoundUnit, MaybeStr
32
35
  from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
33
36
  from utilities.zoneinfo import UTC, get_time_zone_name
34
37
 
@@ -166,6 +169,127 @@ def format_compact(datetime: ZonedDateTime, /) -> str:
166
169
  ##
167
170
 
168
171
 
172
+ class Freq:
173
+ """A rounding frequency."""
174
+
175
+ unit: DateTimeRoundUnit
176
+ increment: int
177
+ _mapping: ClassVar[Mapping[DateTimeRoundUnit, _DateTimeRoundUnitAbbrev]] = {
178
+ "day": "D",
179
+ "hour": "H",
180
+ "minute": "M",
181
+ "second": "S",
182
+ "millisecond": "ms",
183
+ "microsecond": "us",
184
+ "nanosecond": "ns",
185
+ }
186
+
187
+ def __init__(
188
+ self, *, unit: DateTimeRoundUnit = "second", increment: int = 1
189
+ ) -> None:
190
+ super().__init__()
191
+ if (unit == "day") and (increment != 1):
192
+ raise _FreqDayIncrementError(increment=increment)
193
+ if (unit == "hour") and not ((0 < increment < 24) and (24 % increment == 0)):
194
+ raise _FreqIncrementError(unit=unit, increment=increment, divisor=24)
195
+ if (unit in {"minute", "second"}) and not (
196
+ (0 < increment < 60) and (60 % increment == 0)
197
+ ):
198
+ raise _FreqIncrementError(unit=unit, increment=increment, divisor=60)
199
+ if (unit in {"millisecond", "microsecond", "nanosecond"}) and not (
200
+ (0 < increment < 1000) and (1000 % increment == 0)
201
+ ):
202
+ raise _FreqIncrementError(unit=unit, increment=increment, divisor=1000)
203
+ self.unit = unit
204
+ self.increment = increment
205
+
206
+ @override
207
+ def __eq__(self, other: object, /) -> bool:
208
+ if not isinstance(other, Freq):
209
+ return NotImplemented
210
+ return (self.unit == other.unit) and (self.increment == other.increment)
211
+
212
+ @override
213
+ def __hash__(self) -> int:
214
+ return hash((self.unit, self.increment))
215
+
216
+ @override
217
+ def __repr__(self) -> str:
218
+ return f"{type(self).__name__}(unit={self.unit!r}, increment={self.increment})"
219
+
220
+ @classmethod
221
+ def parse(cls, text: str, /) -> Self:
222
+ try:
223
+ increment, abbrev = extract_groups(r"^(\d*)(D|H|M|S|ms|us|ns)$", text)
224
+ except ExtractGroupsError:
225
+ raise _FreqParseError(text=text) from None
226
+ return cls(
227
+ unit=cls._expand(cast("_DateTimeRoundUnitAbbrev", abbrev)),
228
+ increment=int(increment) if len(increment) >= 1 else 1,
229
+ )
230
+
231
+ def serialize(self) -> str:
232
+ if self.increment == 1:
233
+ return self._abbreviation
234
+ return f"{self.increment}{self._abbreviation}"
235
+
236
+ @classmethod
237
+ def _abbreviate(cls, unit: DateTimeRoundUnit, /) -> _DateTimeRoundUnitAbbrev:
238
+ return cls._mapping[unit]
239
+
240
+ @property
241
+ def _abbreviation(self) -> _DateTimeRoundUnitAbbrev:
242
+ return self._mapping[self.unit]
243
+
244
+ @classmethod
245
+ def _expand(cls, unit: _DateTimeRoundUnitAbbrev, /) -> DateTimeRoundUnit:
246
+ values: set[DateTimeRoundUnit] = {
247
+ k for k, v in cls._mapping.items() if v == unit
248
+ }
249
+ (value,) = values
250
+ return value
251
+
252
+
253
+ type FreqLike = MaybeStr[Freq]
254
+ type _DateTimeRoundUnitAbbrev = Literal["D", "H", "M", "S", "ms", "us", "ns"]
255
+
256
+
257
+ @dataclass(kw_only=True, slots=True)
258
+ class FreqError(Exception): ...
259
+
260
+
261
+ @dataclass(kw_only=True, slots=True)
262
+ class _FreqDayIncrementError(FreqError):
263
+ increment: int
264
+
265
+ @override
266
+ def __str__(self) -> str:
267
+ return f"Increment must be 1 for the 'day' unit; got {self.increment}"
268
+
269
+
270
+ @dataclass(kw_only=True, slots=True)
271
+ class _FreqIncrementError(FreqError):
272
+ unit: DateTimeRoundUnit
273
+ increment: int
274
+ divisor: int
275
+
276
+ @override
277
+ def __str__(self) -> str:
278
+ return f"Increment must be a proper divisor of {self.divisor} for the {self.unit!r} unit; got {self.increment}"
279
+
280
+
281
+ @dataclass(kw_only=True, slots=True)
282
+ class _FreqParseError(FreqError):
283
+ text: str
284
+
285
+ @override
286
+ def __str__(self) -> str:
287
+ return f"Unable to parse frequency; got {self.text!r}"
288
+
289
+
290
+ ##
291
+
292
+
169
293
  def from_timestamp(i: float, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
170
294
  """Get a zoned datetime from a timestamp."""
171
295
  return ZonedDateTime.from_timestamp(i, tz=get_time_zone_name(time_zone))
@@ -736,6 +860,9 @@ __all__ = [
736
860
  "ZONED_DATE_TIME_MAX",
737
861
  "ZONED_DATE_TIME_MIN",
738
862
  "DateOrMonth",
863
+ "Freq",
864
+ "FreqError",
865
+ "FreqLike",
739
866
  "MeanDateTimeError",
740
867
  "MinMaxDateError",
741
868
  "Month",