dycw-utilities 0.116.6__py3-none-any.whl → 0.117.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.116.6
3
+ Version: 0.117.1
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=3AZFp33-B-_k917jGyOdDiXXC3IjJevPa1TRWy9GCgs,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=R_UJvKhbhjUKxzotJUoFiE05pVy5Y6rQqwJodAjFMHY,25443
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.1.dist-info/METADATA,sha256=jFmm81hWyjNuttvOK52BPLHNdskY53ciWAA7CLMYy_I,12943
92
+ dycw_utilities-0.117.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.117.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
+ dycw_utilities-0.117.1.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.1"
utilities/asyncio.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime as dt
3
4
  from abc import ABC, abstractmethod
4
5
  from asyncio import (
5
6
  CancelledError,
@@ -32,6 +33,7 @@ from typing import (
32
33
  TYPE_CHECKING,
33
34
  Any,
34
35
  Generic,
36
+ Literal,
35
37
  NoReturn,
36
38
  Self,
37
39
  TextIO,
@@ -41,7 +43,15 @@ from typing import (
41
43
  override,
42
44
  )
43
45
 
44
- from utilities.datetime import MILLISECOND, MINUTE, SECOND, datetime_duration_to_float
46
+ from utilities.datetime import (
47
+ MILLISECOND,
48
+ MINUTE,
49
+ SECOND,
50
+ datetime_duration_to_float,
51
+ datetime_duration_to_timedelta,
52
+ get_now,
53
+ round_datetime,
54
+ )
45
55
  from utilities.errors import ImpossibleCaseError, repr_error
46
56
  from utilities.functions import ensure_int, ensure_not_none, get_class_name
47
57
  from utilities.reprlib import get_repr
@@ -329,12 +339,15 @@ class ExceptionProcessor(QueueProcessor[Exception | type[Exception]]):
329
339
  ##
330
340
 
331
341
 
342
+ type _DurationOrEvery = Duration | tuple[Literal["every"], Duration]
343
+
344
+
332
345
  @dataclass(kw_only=True, unsafe_hash=True)
333
346
  class InfiniteLooper(ABC, Generic[THashable]):
334
347
  """An infinite loop which can throw exceptions by setting events."""
335
348
 
336
- sleep_core: Duration = SECOND
337
- sleep_restart: Duration = MINUTE
349
+ sleep_core: _DurationOrEvery = SECOND
350
+ sleep_restart: _DurationOrEvery = MINUTE
338
351
  logger: str | None = None
339
352
  _events: Mapping[THashable, Event] = field(
340
353
  default_factory=dict, init=False, repr=False, hash=False
@@ -361,7 +374,7 @@ class InfiniteLooper(ABC, Generic[THashable]):
361
374
  await self._initialize()
362
375
  except Exception as error: # noqa: BLE001
363
376
  self._error_upon_initialize(error)
364
- await sleep_dur(duration=self.sleep_restart)
377
+ await self._run_sleep(self.sleep_restart)
365
378
  else:
366
379
  while True:
367
380
  try:
@@ -372,14 +385,14 @@ class InfiniteLooper(ABC, Generic[THashable]):
372
385
  )
373
386
  except StopIteration:
374
387
  await self._core()
375
- await sleep_dur(duration=self.sleep_core)
388
+ await self._run_sleep(self.sleep_core)
376
389
  else:
377
390
  self._raise_error(event)
378
391
  except InfiniteLooperError:
379
392
  raise
380
393
  except Exception as error: # noqa: BLE001
381
394
  self._error_upon_core(error)
382
- await sleep_dur(duration=self.sleep_restart)
395
+ await self._run_sleep(self.sleep_restart)
383
396
 
384
397
  async def _run_looper_with_coroutines(
385
398
  self, *coroutines: Callable[[], Coroutine1[None]]
@@ -393,7 +406,7 @@ class InfiniteLooper(ABC, Generic[THashable]):
393
406
  _ = [tg.create_task(c()) for c in coroutines]
394
407
  except ExceptionGroup as error:
395
408
  self._error_group_upon_coroutines(error)
396
- await sleep_dur(duration=self.sleep_restart)
409
+ await self._run_sleep(self.sleep_restart)
397
410
 
398
411
  async def _initialize(self) -> None:
399
412
  """Initialize the loop."""
@@ -405,20 +418,20 @@ class InfiniteLooper(ABC, Generic[THashable]):
405
418
  """Handle any errors upon initializing the looper."""
406
419
  if self.logger is not None:
407
420
  getLogger(name=self.logger).error(
408
- "%r encountered %r whilst initializing; sleeping for %s...",
421
+ "%r encountered %r whilst initializing; sleeping %s...",
409
422
  get_class_name(self),
410
423
  repr_error(error),
411
- self.sleep_restart,
424
+ self._sleep_restart_desc,
412
425
  )
413
426
 
414
427
  def _error_upon_core(self, error: Exception, /) -> None:
415
428
  """Handle any errors upon running the core function."""
416
429
  if self.logger is not None:
417
430
  getLogger(name=self.logger).error(
418
- "%r encountered %r; sleeping for %s...",
431
+ "%r encountered %r; sleeping %s...",
419
432
  get_class_name(self),
420
433
  repr_error(error),
421
- self.sleep_restart,
434
+ self._sleep_restart_desc,
422
435
  )
423
436
 
424
437
  def _error_group_upon_coroutines(self, group: ExceptionGroup, /) -> None:
@@ -431,7 +444,7 @@ class InfiniteLooper(ABC, Generic[THashable]):
431
444
  f"- Error #{i}/{n}: {repr_error(e)}"
432
445
  for i, e in enumerate(errors, start=1)
433
446
  )
434
- msgs.append(f"Sleeping for {self.sleep_restart}...")
447
+ msgs.append(f"Sleeping {self._sleep_restart_desc}...")
435
448
  getLogger(name=self.logger).error("\n".join(msgs))
436
449
 
437
450
  def _raise_error(self, event: THashable, /) -> NoReturn:
@@ -446,6 +459,29 @@ class InfiniteLooper(ABC, Generic[THashable]):
446
459
  event: Event() for event, _ in self._yield_events_and_exceptions()
447
460
  }
448
461
 
462
+ async def _run_sleep(self, sleep: _DurationOrEvery, /) -> None:
463
+ """Sleep until the next part of the loop."""
464
+ match sleep:
465
+ case int() | float() | dt.timedelta() as duration:
466
+ await sleep_dur(duration=duration)
467
+ case "every", (int() | float() | dt.timedelta()) as duration:
468
+ await sleep_until_rounded(duration)
469
+ case _ as never:
470
+ assert_never(never)
471
+
472
+ @property
473
+ def _sleep_restart_desc(self) -> str:
474
+ """Get a description of the sleep until restart."""
475
+ match self.sleep_restart:
476
+ case int() | float() | dt.timedelta() as duration:
477
+ timedelta = datetime_duration_to_timedelta(duration)
478
+ return f"for {timedelta}"
479
+ case "every", (int() | float() | dt.timedelta()) as duration:
480
+ timedelta = datetime_duration_to_timedelta(duration)
481
+ return f"until next {timedelta}"
482
+ case _ as never:
483
+ assert_never(never)
484
+
449
485
  def _set_event(self, event: THashable, /) -> None:
450
486
  """Set the given event."""
451
487
  try:
@@ -686,6 +722,27 @@ async def sleep_dur(*, duration: Duration | None = None) -> None:
686
722
  ##
687
723
 
688
724
 
725
+ async def sleep_until(datetime: dt.datetime, /) -> None:
726
+ """Sleep until a given time."""
727
+ await sleep_dur(duration=datetime - get_now())
728
+
729
+
730
+ ##
731
+
732
+
733
+ async def sleep_until_rounded(
734
+ duration: Duration, /, *, rel_tol: float | None = None, abs_tol: float | None = None
735
+ ) -> None:
736
+ """Sleep until a rounded time; accepts durations."""
737
+ datetime = round_datetime(
738
+ get_now(), duration, mode="ceil", rel_tol=rel_tol, abs_tol=abs_tol
739
+ )
740
+ await sleep_until(datetime)
741
+
742
+
743
+ ##
744
+
745
+
689
746
  @dataclass(kw_only=True, slots=True)
690
747
  class StreamCommandOutput:
691
748
  process: Process
@@ -768,6 +825,8 @@ __all__ = [
768
825
  "put_items",
769
826
  "put_items_nowait",
770
827
  "sleep_dur",
828
+ "sleep_until",
829
+ "sleep_until_rounded",
771
830
  "stream_command",
772
831
  "timeout_dur",
773
832
  ]
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: