dycw-utilities 0.116.6__py3-none-any.whl → 0.117.0__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.116.6
3
+ Version: 0.117.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,7 +1,7 @@
1
- utilities/__init__.py,sha256=SnbbEgMhiQnik2PTXQdCVRriOVeUKTr4S6_qZSLJwj0,60
1
+ utilities/__init__.py,sha256=P9EtLQQfrOCDM1TAOhodQ4gfhNisheGhmugAZrQJYYc,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
4
- utilities/asyncio.py,sha256=HX5iRmQCbipkbeUgT9Y47KxYvzdqJogysjWygMe5saA,23671
4
+ utilities/asyncio.py,sha256=HqPgdti3ZJPH7uHJkvmZ2weIVKYEpB6FKh6FBriMAPU,24287
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
@@ -12,7 +12,7 @@ utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
12
12
  utilities/cryptography.py,sha256=_CiK_K6c_-uQuUhsUNjNjTL-nqxAh4_1zTfS11Xe120,972
13
13
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
14
  utilities/dataclasses.py,sha256=iiC1wpGXWhaocIikzwBt8bbLWyImoUlOlcDZJGejaIg,33011
15
- utilities/datetime.py,sha256=PcN-4_sSPX1zbpdzBQRdo08pubCuGHyigxkV6SUnvlo,38733
15
+ utilities/datetime.py,sha256=VOwjPibw63Myv-CRYhT2eEHpz277GqUiEDEaI7p-nQw,38985
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
17
  utilities/errors.py,sha256=gxsaa7eq7jbYl41Of40-ivjXqJB5gt4QAcJ0smZZMJE,829
18
18
  utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
@@ -85,10 +85,10 @@ utilities/tzlocal.py,sha256=3upDNFBvGh1l9njmLR2z2S6K6VxQSb7QizYGUbAH3JU,960
85
85
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
86
86
  utilities/version.py,sha256=QFuyEeQA6jI0ruBEcmhqG36f-etg1AEiD1drBBqhQrs,5358
87
87
  utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
88
- utilities/whenever.py,sha256=iLRP_-8CZtBpHKbGZGu-kjSMg1ZubJ-VSmgSy7Eudxw,17787
88
+ utilities/whenever.py,sha256=fC0ZtnO0AyFHsxP4SWj0POI1bf4BIL3Hh4rR51BHfaw,17803
89
89
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
90
90
  utilities/zoneinfo.py,sha256=-Xm57PMMwDTYpxJdkiJG13wnbwK--I7XItBh5WVhD-o,1874
91
- dycw_utilities-0.116.6.dist-info/METADATA,sha256=8P0vlfhUa8DiK1Pnx0T7YwYBvFO5S7xgFgV7PK6CqM0,12943
92
- dycw_utilities-0.116.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
- dycw_utilities-0.116.6.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
- dycw_utilities-0.116.6.dist-info/RECORD,,
91
+ dycw_utilities-0.117.0.dist-info/METADATA,sha256=OB8XvTMe2rLu98khXHV7Y43VtZl_4iotbtejKesZsZg,12943
92
+ dycw_utilities-0.117.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.117.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
+ dycw_utilities-0.117.0.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.116.6"
3
+ __version__ = "0.117.0"
utilities/asyncio.py CHANGED
@@ -41,7 +41,14 @@ from typing import (
41
41
  override,
42
42
  )
43
43
 
44
- from utilities.datetime import MILLISECOND, MINUTE, SECOND, datetime_duration_to_float
44
+ from utilities.datetime import (
45
+ MILLISECOND,
46
+ MINUTE,
47
+ SECOND,
48
+ datetime_duration_to_float,
49
+ get_now,
50
+ round_datetime,
51
+ )
45
52
  from utilities.errors import ImpossibleCaseError, repr_error
46
53
  from utilities.functions import ensure_int, ensure_not_none, get_class_name
47
54
  from utilities.reprlib import get_repr
@@ -55,6 +62,7 @@ from utilities.types import (
55
62
  )
56
63
 
57
64
  if TYPE_CHECKING:
65
+ import datetime as dt
58
66
  from asyncio import _CoroutineLike
59
67
  from asyncio.subprocess import Process
60
68
  from collections.abc import AsyncIterator, Sequence
@@ -686,6 +694,27 @@ async def sleep_dur(*, duration: Duration | None = None) -> None:
686
694
  ##
687
695
 
688
696
 
697
+ async def sleep_until(datetime: dt.datetime, /) -> None:
698
+ """Sleep until a given time."""
699
+ await sleep_dur(duration=datetime - get_now())
700
+
701
+
702
+ ##
703
+
704
+
705
+ async def sleep_until_rounded(
706
+ duration: Duration, /, *, rel_tol: float | None = None, abs_tol: float | None = None
707
+ ) -> None:
708
+ """Sleep until a rounded time; accepts durations."""
709
+ datetime = round_datetime(
710
+ get_now(), duration, mode="ceil", rel_tol=rel_tol, abs_tol=abs_tol
711
+ )
712
+ await sleep_until(datetime)
713
+
714
+
715
+ ##
716
+
717
+
689
718
  @dataclass(kw_only=True, slots=True)
690
719
  class StreamCommandOutput:
691
720
  process: Process
@@ -768,6 +797,8 @@ __all__ = [
768
797
  "put_items",
769
798
  "put_items_nowait",
770
799
  "sleep_dur",
800
+ "sleep_until",
801
+ "sleep_until_rounded",
771
802
  "stream_command",
772
803
  "timeout_dur",
773
804
  ]
utilities/datetime.py CHANGED
@@ -334,6 +334,52 @@ def datetime_duration_to_float(duration: Duration, /) -> float:
334
334
  assert_never(never)
335
335
 
336
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
+
337
383
  def datetime_duration_to_timedelta(duration: Duration, /) -> dt.timedelta:
338
384
  """Ensure a datetime duration is a timedelta."""
339
385
  match duration:
@@ -651,8 +697,9 @@ YEAR = get_years(n=1)
651
697
  ##
652
698
 
653
699
 
654
- def is_integral_timedelta(timedelta: dt.timedelta, /) -> bool:
655
- """Check if a timedelta is integral."""
700
+ def is_integral_timedelta(duration: Duration, /) -> bool:
701
+ """Check if a duration is integral."""
702
+ timedelta = datetime_duration_to_timedelta(duration)
656
703
  return (timedelta.seconds == 0) and (timedelta.microseconds == 0)
657
704
 
658
705
 
@@ -679,9 +726,9 @@ def is_weekday(date: dt.date, /) -> bool:
679
726
  ##
680
727
 
681
728
 
682
- def is_zero_time(timedelta: dt.timedelta, /) -> bool:
729
+ def is_zero_time(duration: Duration, /) -> bool:
683
730
  """Check if a timedelta is 0."""
684
- return timedelta == ZERO_TIME
731
+ return datetime_duration_to_timedelta(duration) == ZERO_TIME
685
732
 
686
733
 
687
734
  ##
@@ -763,7 +810,7 @@ def mean_timedelta(
763
810
  case 1:
764
811
  return one(timedeltas)
765
812
  case _:
766
- microseconds = list(map(timedelta_to_microseconds, timedeltas))
813
+ microseconds = list(map(datetime_duration_to_microseconds, timedeltas))
767
814
  mean_float = fmean(microseconds, weights=weights)
768
815
  mean_int = round_(mean_float, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol)
769
816
  return microseconds_to_timedelta(mean_int)
@@ -781,7 +828,7 @@ class MeanTimeDeltaError(Exception):
781
828
 
782
829
  def microseconds_since_epoch(datetime: dt.datetime, /) -> int:
783
830
  """Compute the number of microseconds since the epoch."""
784
- return timedelta_to_microseconds(timedelta_since_epoch(datetime))
831
+ return datetime_duration_to_microseconds(timedelta_since_epoch(datetime))
785
832
 
786
833
 
787
834
  def microseconds_to_timedelta(microseconds: int, /) -> dt.timedelta:
@@ -980,7 +1027,7 @@ class _ParseTwoDigitYearInvalidStringError(Exception):
980
1027
 
981
1028
  def round_datetime(
982
1029
  datetime: dt.datetime,
983
- timedelta: dt.timedelta,
1030
+ duration: Duration,
984
1031
  /,
985
1032
  *,
986
1033
  mode: RoundMode = "standard",
@@ -990,7 +1037,7 @@ def round_datetime(
990
1037
  """Round a datetime to a timedelta."""
991
1038
  if datetime.tzinfo is None:
992
1039
  dividend = microseconds_since_epoch(datetime)
993
- divisor = timedelta_to_microseconds(timedelta)
1040
+ divisor = datetime_duration_to_microseconds(duration)
994
1041
  quotient, remainder = divmod(dividend, divisor)
995
1042
  rnd_remainder = round_(
996
1043
  remainder / divisor, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
@@ -1000,7 +1047,7 @@ def round_datetime(
1000
1047
  return microseconds_since_epoch_to_datetime(microseconds)
1001
1048
  local = datetime.replace(tzinfo=None)
1002
1049
  rounded = round_datetime(
1003
- local, timedelta, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
1050
+ local, duration, mode=mode, rel_tol=rel_tol, abs_tol=abs_tol
1004
1051
  )
1005
1052
  return rounded.replace(tzinfo=datetime.tzinfo)
1006
1053
 
@@ -1175,50 +1222,6 @@ def timedelta_since_epoch(date_or_datetime: DateOrDateTime, /) -> dt.timedelta:
1175
1222
  assert_never(never)
1176
1223
 
1177
1224
 
1178
- def timedelta_to_microseconds(timedelta: dt.timedelta, /) -> int:
1179
- """Compute the number of microseconds in a timedelta."""
1180
- return (
1181
- _MICROSECONDS_PER_DAY * timedelta.days
1182
- + _MICROSECONDS_PER_SECOND * timedelta.seconds
1183
- + timedelta.microseconds
1184
- )
1185
-
1186
-
1187
- @overload
1188
- def timedelta_to_milliseconds(
1189
- timedelta: dt.timedelta, /, *, strict: Literal[True]
1190
- ) -> int: ...
1191
- @overload
1192
- def timedelta_to_milliseconds(
1193
- timedelta: dt.timedelta, /, *, strict: bool = False
1194
- ) -> float: ...
1195
- def timedelta_to_milliseconds(
1196
- timedelta: dt.timedelta, /, *, strict: bool = False
1197
- ) -> int | float:
1198
- """Compute the number of milliseconds in a timedelta."""
1199
- microseconds = timedelta_to_microseconds(timedelta)
1200
- milliseconds, remainder = divmod(microseconds, _MICROSECONDS_PER_MILLISECOND)
1201
- match remainder, strict:
1202
- case 0, _:
1203
- return milliseconds
1204
- case _, True:
1205
- raise TimedeltaToMillisecondsError(timedelta=timedelta, remainder=remainder)
1206
- case _, False:
1207
- return milliseconds + remainder / _MICROSECONDS_PER_MILLISECOND
1208
- case _ as never:
1209
- assert_never(never)
1210
-
1211
-
1212
- @dataclass(kw_only=True, slots=True)
1213
- class TimedeltaToMillisecondsError(Exception):
1214
- timedelta: dt.timedelta
1215
- remainder: int
1216
-
1217
- @override
1218
- def __str__(self) -> str:
1219
- return f"Unable to convert {self.timedelta} to milliseconds; got {self.remainder} microsecond(s)"
1220
-
1221
-
1222
1225
  ##
1223
1226
 
1224
1227
 
@@ -1367,6 +1370,8 @@ __all__ = [
1367
1370
  "date_to_datetime",
1368
1371
  "date_to_month",
1369
1372
  "datetime_duration_to_float",
1373
+ "datetime_duration_to_microseconds",
1374
+ "datetime_duration_to_milliseconds",
1370
1375
  "datetime_duration_to_timedelta",
1371
1376
  "datetime_utc",
1372
1377
  "days_since_epoch",
@@ -1407,8 +1412,6 @@ __all__ = [
1407
1412
  "serialize_month",
1408
1413
  "sub_duration",
1409
1414
  "timedelta_since_epoch",
1410
- "timedelta_to_microseconds",
1411
- "timedelta_to_milliseconds",
1412
1415
  "yield_days",
1413
1416
  "yield_weekdays",
1414
1417
  ]
utilities/whenever.py CHANGED
@@ -14,8 +14,8 @@ from utilities.datetime import (
14
14
  _MICROSECONDS_PER_SECOND,
15
15
  ZERO_TIME,
16
16
  check_date_not_datetime,
17
+ datetime_duration_to_microseconds,
17
18
  parse_two_digit_year,
18
- timedelta_to_microseconds,
19
19
  )
20
20
  from utilities.math import ParseNumberError, parse_number
21
21
  from utilities.re import (
@@ -601,7 +601,7 @@ class SerializeZonedDateTimeError(Exception):
601
601
 
602
602
  def _to_datetime_delta(timedelta: dt.timedelta, /) -> DateTimeDelta:
603
603
  """Serialize a timedelta."""
604
- total_microseconds = timedelta_to_microseconds(timedelta)
604
+ total_microseconds = datetime_duration_to_microseconds(timedelta)
605
605
  if total_microseconds == 0:
606
606
  return DateTimeDelta()
607
607
  if total_microseconds >= 1: