dycw-utilities 0.113.2__py3-none-any.whl → 0.113.4__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,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.113.2
3
+ Version: 0.113.4
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
7
7
  Requires-Dist: typing-extensions<4.14,>=4.13.1
8
8
  Provides-Extra: test
9
- Requires-Dist: hypothesis<6.132,>=6.131.10; extra == 'test'
9
+ Requires-Dist: hypothesis<6.132,>=6.131.15; extra == 'test'
10
10
  Requires-Dist: pytest-asyncio<0.27,>=0.26.0; extra == 'test'
11
11
  Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
12
12
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
@@ -14,7 +14,7 @@ Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.2; extra == 'test'
14
14
  Requires-Dist: pytest-only<2.2,>=2.1.2; extra == 'test'
15
15
  Requires-Dist: pytest-randomly<3.17,>=3.16.0; extra == 'test'
16
16
  Requires-Dist: pytest-regressions<2.8,>=2.7.0; extra == 'test'
17
- Requires-Dist: pytest-rerunfailures<16,>=15.0; extra == 'test'
17
+ Requires-Dist: pytest-rerunfailures<16,>=15.1; extra == 'test'
18
18
  Requires-Dist: pytest-rng<1.1,>=1.0.0; extra == 'test'
19
19
  Requires-Dist: pytest-timeout<2.5,>=2.4.0; extra == 'test'
20
20
  Requires-Dist: pytest-xdist<3.7,>=3.6.1; extra == 'test'
@@ -37,7 +37,7 @@ Requires-Dist: atools<0.15,>=0.14.2; extra == 'zzz-test-atools'
37
37
  Provides-Extra: zzz-test-cachetools
38
38
  Requires-Dist: cachetools<5.6,>=5.5.2; extra == 'zzz-test-cachetools'
39
39
  Provides-Extra: zzz-test-click
40
- Requires-Dist: click<8.2,>=8.1.8; extra == 'zzz-test-click'
40
+ Requires-Dist: click<8.3,>=8.2.0; extra == 'zzz-test-click'
41
41
  Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-click'
42
42
  Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-click'
43
43
  Provides-Extra: zzz-test-contextlib
@@ -80,7 +80,7 @@ Provides-Extra: zzz-test-hypothesis
80
80
  Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-hypothesis'
81
81
  Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
82
82
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
83
- Requires-Dist: hypothesis<6.132,>=6.131.10; extra == 'zzz-test-hypothesis'
83
+ Requires-Dist: hypothesis<6.132,>=6.131.15; extra == 'zzz-test-hypothesis'
84
84
  Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
85
85
  Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-hypothesis'
86
86
  Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
@@ -101,7 +101,7 @@ Requires-Dist: polars-lts-cpu<1.30,>=1.29.0; extra == 'zzz-test-jupyter'
101
101
  Provides-Extra: zzz-test-logging
102
102
  Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-logging'
103
103
  Requires-Dist: coloredlogs<15.1,>=15.0.1; extra == 'zzz-test-logging'
104
- Requires-Dist: concurrent-log-handler<0.10,>=0.9.25; extra == 'zzz-test-logging'
104
+ Requires-Dist: concurrent-log-handler<0.10,>=0.9.26; extra == 'zzz-test-logging'
105
105
  Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-logging'
106
106
  Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-logging'
107
107
  Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-logging'
@@ -172,7 +172,7 @@ Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-redis'
172
172
  Provides-Extra: zzz-test-rich
173
173
  Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-rich'
174
174
  Provides-Extra: zzz-test-scipy
175
- Requires-Dist: scipy<1.16,>=1.15.2; extra == 'zzz-test-scipy'
175
+ Requires-Dist: scipy<1.16,>=1.15.3; extra == 'zzz-test-scipy'
176
176
  Provides-Extra: zzz-test-sentinel
177
177
  Provides-Extra: zzz-test-shelve
178
178
  Provides-Extra: zzz-test-slack-sdk
@@ -1,18 +1,18 @@
1
- utilities/__init__.py,sha256=gPP9gUduOhrk3TNScyIM28aOLTbYa1tnDF9Vu80gxZU,60
1
+ utilities/__init__.py,sha256=f-gfMxj416K6fDUn3GBZ_ctE4iCAIW3yKwMD5u5-rOw,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
4
4
  utilities/asyncio.py,sha256=6Pv0xK2jP55iF6EYjY1g6_rFGOeJyihxRNxXnOsWv14,15691
5
5
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
6
6
  utilities/atools.py,sha256=IYMuFSFGSKyuQmqD6v5IUtDlz8PPw0Sr87Cub_gRU3M,1168
7
7
  utilities/cachetools.py,sha256=C1zqOg7BYz0IfQFK8e3qaDDgEZxDpo47F15RTfJM37Q,2910
8
- utilities/click.py,sha256=lIkJIp_3vaoqsvADI4azoLgEWqlUHVRWBQePLbPrcPo,14243
8
+ utilities/click.py,sha256=Jzm7DOI3rH-WLOTh1UHOfxkFhHU4_GJH60seLV0A-OI,14311
9
9
  utilities/concurrent.py,sha256=s2scTEd2AhXVTW4hpASU2qxV_DiVLALfms55cCQzCvM,2886
10
10
  utilities/contextlib.py,sha256=OOIIEa5lXKGzFAnauaul40nlQnQko6Na4ryiMJcHkIg,478
11
11
  utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
12
12
  utilities/cryptography.py,sha256=HyOewI20cl3uRXsKivhIaeLVDInQdzgXZGaly7hS5dE,771
13
13
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
14
  utilities/dataclasses.py,sha256=iiC1wpGXWhaocIikzwBt8bbLWyImoUlOlcDZJGejaIg,33011
15
- utilities/datetime.py,sha256=OF7jZE702UecnwAbq9D3N-GINpp9gSGoidki1RhimCE,35752
15
+ utilities/datetime.py,sha256=PcN-4_sSPX1zbpdzBQRdo08pubCuGHyigxkV6SUnvlo,38733
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
17
  utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
18
18
  utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
@@ -58,7 +58,7 @@ utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq
58
58
  utilities/python_dotenv.py,sha256=iWcnpXbH7S6RoXHiLlGgyuH6udCupAcPd_gQ0eAenQ0,3190
59
59
  utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
60
60
  utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
61
- utilities/redis.py,sha256=jpBq5dEKthxLgS68Bj-dksfGqFl-RrRGK-x5-6owDVI,25438
61
+ utilities/redis.py,sha256=otrtsfGvU5FBzqDJ7IEMQOJwG5ccFaq4TRKEMteqYk8,25489
62
62
  utilities/reprlib.py,sha256=Re9bk3n-kC__9DxQmRlevqFA86pE6TtVfWjUgpbVOv0,1849
63
63
  utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
64
64
  utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
@@ -87,7 +87,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
87
  utilities/whenever.py,sha256=iLRP_-8CZtBpHKbGZGu-kjSMg1ZubJ-VSmgSy7Eudxw,17787
88
88
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
89
  utilities/zoneinfo.py,sha256=-Xm57PMMwDTYpxJdkiJG13wnbwK--I7XItBh5WVhD-o,1874
90
- dycw_utilities-0.113.2.dist-info/METADATA,sha256=QgO7W2WhDgyW3k4-rB8FUJXk-vqpi7MfZfthSYdAD2o,12943
91
- dycw_utilities-0.113.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.113.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.113.2.dist-info/RECORD,,
90
+ dycw_utilities-0.113.4.dist-info/METADATA,sha256=ErT951cvoqudHh2P5_G-_Jhl0SziHUJZ8O8lJV4cGi8,12943
91
+ dycw_utilities-0.113.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.113.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.113.4.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.113.2"
3
+ __version__ = "0.113.4"
utilities/click.py CHANGED
@@ -125,7 +125,8 @@ class Enum(ParamType, Generic[TEnum]):
125
125
  self.fail(str(error), param, ctx)
126
126
 
127
127
  @override
128
- def get_metavar(self, param: Parameter) -> str | None:
128
+ def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
129
+ _ = ctx
129
130
  desc = ",".join(e.name for e in self._enum)
130
131
  return _make_metavar(param, desc)
131
132
 
@@ -273,8 +274,8 @@ class FrozenSetParameter(ParamType, Generic[_TParam, _T]):
273
274
  return frozenset(self._param.convert(v, param, ctx) for v in values)
274
275
 
275
276
  @override
276
- def get_metavar(self, param: Parameter) -> str | None:
277
- if (metavar := self._param.get_metavar(param)) is None:
277
+ def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
278
+ if (metavar := self._param.get_metavar(param, ctx)) is None:
278
279
  name = self.name.upper()
279
280
  else:
280
281
  name = f"FROZENSET{metavar}"
@@ -397,8 +398,8 @@ class ListParameter(ParamType, Generic[_TParam, _T]):
397
398
  return [self._param.convert(v, param, ctx) for v in values]
398
399
 
399
400
  @override
400
- def get_metavar(self, param: Parameter) -> str | None:
401
- if (metavar := self._param.get_metavar(param)) is None:
401
+ def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
402
+ if (metavar := self._param.get_metavar(param, ctx)) is None:
402
403
  name = self.name.upper()
403
404
  else:
404
405
  name = f"LIST{metavar}"
utilities/datetime.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
- from collections.abc import Callable, Iterable
4
+ from collections.abc import Callable, Iterable, Sequence
5
5
  from dataclasses import dataclass, replace
6
6
  from re import search, sub
7
7
  from statistics import fmean
@@ -495,6 +495,101 @@ def get_half_years(*, n: int = 1) -> dt.timedelta:
495
495
 
496
496
  HALF_YEAR = get_half_years(n=1)
497
497
 
498
+ ##
499
+
500
+
501
+ def get_min_max_date(
502
+ *,
503
+ min_date: dt.date | None = None,
504
+ max_date: dt.date | None = None,
505
+ min_age: Duration | None = None,
506
+ max_age: Duration | None = None,
507
+ time_zone: TimeZoneLike = UTC,
508
+ ) -> tuple[dt.date | None, dt.date | None]:
509
+ """Get the min/max date given a combination of dates/ages."""
510
+ today = get_today(time_zone=time_zone)
511
+ min_parts: Sequence[dt.date] = []
512
+ if min_date is not None:
513
+ if min_date > today:
514
+ raise _GetMinMaxDateMinDateError(min_date=min_date, today=today)
515
+ min_parts.append(min_date)
516
+ if max_age is not None:
517
+ if date_duration_to_timedelta(max_age) < ZERO_TIME:
518
+ raise _GetMinMaxDateMaxAgeError(max_age=max_age)
519
+ min_parts.append(sub_duration(today, duration=max_age))
520
+ min_date_use = max(min_parts, default=None)
521
+ max_parts: Sequence[dt.date] = []
522
+ if max_date is not None:
523
+ if max_date > today:
524
+ raise _GetMinMaxDateMaxDateError(max_date=max_date, today=today)
525
+ max_parts.append(max_date)
526
+ if min_age is not None:
527
+ if date_duration_to_timedelta(min_age) < ZERO_TIME:
528
+ raise _GetMinMaxDateMinAgeError(min_age=min_age)
529
+ max_parts.append(sub_duration(today, duration=min_age))
530
+ max_date_use = min(max_parts, default=None)
531
+ if (
532
+ (min_date_use is not None)
533
+ and (max_date_use is not None)
534
+ and (min_date_use > max_date_use)
535
+ ):
536
+ raise _GetMinMaxDatePeriodError(min_date=min_date_use, max_date=max_date_use)
537
+ return min_date_use, max_date_use
538
+
539
+
540
+ @dataclass(kw_only=True, slots=True)
541
+ class GetMinMaxDateError(Exception): ...
542
+
543
+
544
+ @dataclass(kw_only=True, slots=True)
545
+ class _GetMinMaxDateMinDateError(GetMinMaxDateError):
546
+ min_date: dt.date
547
+ today: dt.date
548
+
549
+ @override
550
+ def __str__(self) -> str:
551
+ return f"Min date must be at most today; got {self.min_date} > {self.today}"
552
+
553
+
554
+ @dataclass(kw_only=True, slots=True)
555
+ class _GetMinMaxDateMinAgeError(GetMinMaxDateError):
556
+ min_age: Duration
557
+
558
+ @override
559
+ def __str__(self) -> str:
560
+ return f"Min age must be non-negative; got {self.min_age}"
561
+
562
+
563
+ @dataclass(kw_only=True, slots=True)
564
+ class _GetMinMaxDateMaxDateError(GetMinMaxDateError):
565
+ max_date: dt.date
566
+ today: dt.date
567
+
568
+ @override
569
+ def __str__(self) -> str:
570
+ return f"Max date must be at most today; got {self.max_date} > {self.today}"
571
+
572
+
573
+ @dataclass(kw_only=True, slots=True)
574
+ class _GetMinMaxDateMaxAgeError(GetMinMaxDateError):
575
+ max_age: Duration
576
+
577
+ @override
578
+ def __str__(self) -> str:
579
+ return f"Max age must be non-negative; got {self.max_age}"
580
+
581
+
582
+ @dataclass(kw_only=True, slots=True)
583
+ class _GetMinMaxDatePeriodError(GetMinMaxDateError):
584
+ min_date: dt.date
585
+ max_date: dt.date
586
+
587
+ @override
588
+ def __str__(self) -> str:
589
+ return (
590
+ f"Min date must be at most max date; got {self.min_date} > {self.max_date}"
591
+ )
592
+
498
593
 
499
594
  ##
500
595
 
@@ -1244,6 +1339,7 @@ __all__ = [
1244
1339
  "CheckDateNotDateTimeError",
1245
1340
  "DateOrMonth",
1246
1341
  "EnsureMonthError",
1342
+ "GetMinMaxDateError",
1247
1343
  "MeanDateTimeError",
1248
1344
  "MeanTimeDeltaError",
1249
1345
  "MillisecondsSinceEpochError",
@@ -1280,6 +1376,7 @@ __all__ = [
1280
1376
  "get_date",
1281
1377
  "get_datetime",
1282
1378
  "get_half_years",
1379
+ "get_min_max_date",
1283
1380
  "get_months",
1284
1381
  "get_now",
1285
1382
  "get_quarters",
utilities/redis.py CHANGED
@@ -607,7 +607,7 @@ class Publisher(QueueProcessor[tuple[str, EncodableT]]):
607
607
 
608
608
 
609
609
  _SUBSCRIBE_TIMEOUT: Duration = SECOND
610
- _SUBSCRIBE_SLEEP: Duration = 10 * MILLISECOND
610
+ _SUBSCRIBE_SLEEP: Duration = MILLISECOND
611
611
 
612
612
 
613
613
  @overload
@@ -617,8 +617,8 @@ def subscribe(
617
617
  /,
618
618
  *,
619
619
  deserializer: Callable[[bytes], _T],
620
- timeout: Duration | None = ...,
621
- sleep: Duration = ...,
620
+ timeout: Duration | None = _SUBSCRIBE_TIMEOUT,
621
+ sleep: Duration = _SUBSCRIBE_SLEEP,
622
622
  ) -> AsyncIterator[_T]: ...
623
623
  @overload
624
624
  def subscribe(
@@ -627,8 +627,8 @@ def subscribe(
627
627
  /,
628
628
  *,
629
629
  deserializer: None = None,
630
- timeout: Duration | None = ...,
631
- sleep: Duration = ...,
630
+ timeout: Duration | None = _SUBSCRIBE_TIMEOUT,
631
+ sleep: Duration = _SUBSCRIBE_SLEEP,
632
632
  ) -> AsyncIterator[bytes]: ...
633
633
  async def subscribe(
634
634
  pubsub: PubSub,