dycw-utilities 0.166.30__py3-none-any.whl → 0.185.8__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.
Files changed (96) hide show
  1. dycw_utilities-0.185.8.dist-info/METADATA +33 -0
  2. dycw_utilities-0.185.8.dist-info/RECORD +90 -0
  3. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
  4. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +17 -10
  7. utilities/asyncio.py +50 -72
  8. utilities/atools.py +9 -11
  9. utilities/cachetools.py +16 -11
  10. utilities/click.py +76 -19
  11. utilities/concurrent.py +1 -1
  12. utilities/constants.py +492 -0
  13. utilities/contextlib.py +23 -30
  14. utilities/contextvars.py +1 -23
  15. utilities/core.py +2581 -0
  16. utilities/dataclasses.py +16 -119
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +1 -1
  19. utilities/errors.py +2 -16
  20. utilities/fastapi.py +5 -5
  21. utilities/fpdf2.py +2 -1
  22. utilities/functions.py +34 -265
  23. utilities/http.py +2 -3
  24. utilities/hypothesis.py +84 -29
  25. utilities/importlib.py +17 -1
  26. utilities/iterables.py +39 -575
  27. utilities/jinja2.py +145 -0
  28. utilities/jupyter.py +5 -3
  29. utilities/libcst.py +1 -1
  30. utilities/lightweight_charts.py +4 -6
  31. utilities/logging.py +24 -24
  32. utilities/math.py +1 -36
  33. utilities/more_itertools.py +4 -6
  34. utilities/numpy.py +2 -1
  35. utilities/operator.py +2 -2
  36. utilities/orjson.py +42 -43
  37. utilities/os.py +4 -147
  38. utilities/packaging.py +129 -0
  39. utilities/parse.py +35 -15
  40. utilities/pathlib.py +3 -120
  41. utilities/platform.py +8 -90
  42. utilities/polars.py +38 -32
  43. utilities/postgres.py +37 -33
  44. utilities/pottery.py +20 -18
  45. utilities/pqdm.py +3 -4
  46. utilities/psutil.py +2 -3
  47. utilities/pydantic.py +25 -0
  48. utilities/pydantic_settings.py +87 -16
  49. utilities/pydantic_settings_sops.py +16 -3
  50. utilities/pyinstrument.py +4 -4
  51. utilities/pytest.py +96 -125
  52. utilities/pytest_plugins/pytest_regressions.py +2 -2
  53. utilities/pytest_regressions.py +32 -11
  54. utilities/random.py +2 -8
  55. utilities/redis.py +98 -94
  56. utilities/reprlib.py +11 -118
  57. utilities/shellingham.py +66 -0
  58. utilities/shutil.py +25 -0
  59. utilities/slack_sdk.py +13 -12
  60. utilities/sqlalchemy.py +57 -30
  61. utilities/sqlalchemy_polars.py +16 -25
  62. utilities/subprocess.py +2590 -0
  63. utilities/tabulate.py +32 -0
  64. utilities/testbook.py +8 -8
  65. utilities/text.py +24 -99
  66. utilities/throttle.py +159 -0
  67. utilities/time.py +18 -0
  68. utilities/timer.py +31 -14
  69. utilities/traceback.py +16 -23
  70. utilities/types.py +42 -2
  71. utilities/typing.py +26 -14
  72. utilities/uuid.py +1 -1
  73. utilities/version.py +202 -45
  74. utilities/whenever.py +53 -150
  75. dycw_utilities-0.166.30.dist-info/METADATA +0 -41
  76. dycw_utilities-0.166.30.dist-info/RECORD +0 -98
  77. dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
  78. utilities/aeventkit.py +0 -388
  79. utilities/atomicwrites.py +0 -182
  80. utilities/cryptography.py +0 -41
  81. utilities/getpass.py +0 -8
  82. utilities/git.py +0 -19
  83. utilities/gzip.py +0 -31
  84. utilities/json.py +0 -70
  85. utilities/pickle.py +0 -25
  86. utilities/re.py +0 -156
  87. utilities/sentinel.py +0 -73
  88. utilities/socket.py +0 -8
  89. utilities/string.py +0 -20
  90. utilities/tempfile.py +0 -77
  91. utilities/typed_settings.py +0 -152
  92. utilities/tzdata.py +0 -11
  93. utilities/tzlocal.py +0 -28
  94. utilities/warnings.py +0 -65
  95. utilities/zipfile.py +0 -25
  96. utilities/zoneinfo.py +0 -133
utilities/tabulate.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from textwrap import indent
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from tabulate import tabulate
7
+
8
+ from utilities.core import get_func_name, normalize_str
9
+ from utilities.text import split_f_str_equals
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+
15
+ def func_param_desc(func: Callable[..., Any], version: str, /, *variables: str) -> str:
16
+ """Generate a string describing a function & its parameters."""
17
+ name = get_func_name(func)
18
+ table = indent(params_table(*variables), " ")
19
+ return normalize_str(f"""\
20
+ Running {name!r} (version {version}) with:
21
+ {table}
22
+ """)
23
+
24
+
25
+ def params_table(*variables: str) -> str:
26
+ """Generate a table of parameter names and values."""
27
+ data = list(map(split_f_str_equals, variables))
28
+ table = tabulate(data, tablefmt="rounded_outline")
29
+ return normalize_str(table)
30
+
31
+
32
+ __all__ = ["func_param_desc", "params_table"]
utilities/testbook.py CHANGED
@@ -5,17 +5,17 @@ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from testbook import testbook
7
7
 
8
- from utilities.pytest import throttle
9
- from utilities.text import pascal_case
8
+ from utilities.core import pascal_case
9
+ from utilities.pytest import throttle_test
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Callable
13
13
 
14
- from utilities.types import Delta, PathLike
14
+ from utilities.types import Duration, PathLike
15
15
 
16
16
 
17
17
  def build_notebook_tester(
18
- path: PathLike, /, *, throttle: Delta | None = None, on_try: bool = False
18
+ path: PathLike, /, *, throttle: Duration | None = None, on_try: bool = False
19
19
  ) -> type[Any]:
20
20
  """Build the notebook tester class."""
21
21
  path = Path(path)
@@ -27,7 +27,7 @@ def build_notebook_tester(
27
27
  ]
28
28
  namespace = {
29
29
  f"test_{p.stem.replace('-', '_')}": _build_test_method(
30
- p, delta=throttle, on_try=on_try
30
+ p, duration=throttle, on_try=on_try
31
31
  )
32
32
  for p in notebooks
33
33
  }
@@ -35,14 +35,14 @@ def build_notebook_tester(
35
35
 
36
36
 
37
37
  def _build_test_method(
38
- path: Path, /, *, delta: Delta | None = None, on_try: bool = False
38
+ path: Path, /, *, duration: Duration | None = None, on_try: bool = False
39
39
  ) -> Callable[..., Any]:
40
40
  @testbook(path, execute=True)
41
41
  def method(self: Any, tb: Any) -> None:
42
42
  _ = (self, tb) # pragma: no cover
43
43
 
44
- if delta is not None:
45
- method = throttle(delta=delta, on_try=on_try)(method)
44
+ if duration is not None:
45
+ method = throttle_test(duration=duration, on_try=on_try)(method)
46
46
 
47
47
  return method
48
48
 
utilities/text.py CHANGED
@@ -5,11 +5,7 @@ from collections import deque
5
5
  from collections.abc import Callable
6
6
  from dataclasses import dataclass
7
7
  from itertools import chain
8
- from os import getpid
9
- from re import IGNORECASE, VERBOSE, escape, search
10
- from textwrap import dedent
11
- from threading import get_ident
12
- from time import time_ns
8
+ from re import IGNORECASE, escape, search
13
9
  from typing import (
14
10
  TYPE_CHECKING,
15
11
  Any,
@@ -19,11 +15,10 @@ from typing import (
19
15
  overload,
20
16
  override,
21
17
  )
22
- from uuid import uuid4
23
18
 
24
- from utilities.iterables import CheckDuplicatesError, check_duplicates, transpose
25
- from utilities.reprlib import get_repr
26
- from utilities.sentinel import Sentinel
19
+ from utilities.constants import BRACKETS, LIST_SEPARATOR, PAIR_SEPARATOR, Sentinel
20
+ from utilities.core import repr_, transpose
21
+ from utilities.iterables import CheckDuplicatesError, check_duplicates
27
22
 
28
23
  if TYPE_CHECKING:
29
24
  from collections.abc import Iterable, Mapping, Sequence
@@ -31,9 +26,6 @@ if TYPE_CHECKING:
31
26
  from utilities.types import MaybeCallableBoolLike, MaybeCallableStr, StrStrMapping
32
27
 
33
28
 
34
- DEFAULT_SEPARATOR = ","
35
-
36
-
37
29
  ##
38
30
 
39
31
 
@@ -77,21 +69,6 @@ class ParseNoneError(Exception):
77
69
  ##
78
70
 
79
71
 
80
- def pascal_case(text: str, /) -> str:
81
- """Convert text to pascal case."""
82
- parts = _SPLIT_TEXT.findall(text)
83
- parts = [p for p in parts if len(p) >= 1]
84
- parts = list(map(_pascal_case_one, parts))
85
- return "".join(parts)
86
-
87
-
88
- def _pascal_case_one(text: str, /) -> str:
89
- return text if text.isupper() else text.title()
90
-
91
-
92
- ##
93
-
94
-
95
72
  def prompt_bool(prompt: object = "", /, *, confirm: bool = False) -> bool:
96
73
  """Prompt for a boolean."""
97
74
  return True if confirm else parse_bool(input(prompt))
@@ -100,47 +77,21 @@ def prompt_bool(prompt: object = "", /, *, confirm: bool = False) -> bool:
100
77
  ##
101
78
 
102
79
 
103
- def repr_encode(obj: Any, /) -> bytes:
104
- """Return the representation of the object encoded as bytes."""
105
- return repr(obj).encode()
80
+ def split_f_str_equals(text: str, /) -> tuple[str, str]:
81
+ """Split an `f`-string with `=`."""
82
+ first, second = text.split(sep="=", maxsplit=1)
83
+ return first, second
106
84
 
107
85
 
108
86
  ##
109
87
 
110
88
 
111
- def snake_case(text: str, /) -> str:
112
- """Convert text into snake case."""
113
- leading = bool(search(r"^_", text))
114
- trailing = bool(search(r"_$", text))
115
- parts = _SPLIT_TEXT.findall(text)
116
- parts = (p for p in parts if len(p) >= 1)
117
- parts = chain([""] if leading else [], parts, [""] if trailing else [])
118
- return "_".join(parts).lower()
119
-
120
-
121
- _SPLIT_TEXT = re.compile(
122
- r"""
123
- [A-Z]+(?=[A-Z][a-z0-9]) | # all caps followed by Upper+lower or digit (API in APIResponse2)
124
- [A-Z]?[a-z]+[0-9]* | # normal words with optional trailing digits (Text123)
125
- [A-Z]+[0-9]* | # consecutive caps with optional trailing digits (ID2)
126
- """,
127
- flags=VERBOSE,
128
- )
129
-
130
- ##
131
-
132
-
133
- LIST_SEPARATOR = DEFAULT_SEPARATOR
134
- PAIR_SEPARATOR = "="
135
- BRACKETS = [("(", ")"), ("[", "]"), ("{", "}")]
136
-
137
-
138
89
  @overload
139
90
  def split_key_value_pairs(
140
91
  text: str,
141
92
  /,
142
93
  *,
143
- list_separator: str = DEFAULT_SEPARATOR,
94
+ list_separator: str = LIST_SEPARATOR,
144
95
  pair_separator: str = PAIR_SEPARATOR,
145
96
  brackets: Iterable[tuple[str, str]] | None = BRACKETS,
146
97
  mapping: Literal[True],
@@ -150,7 +101,7 @@ def split_key_value_pairs(
150
101
  text: str,
151
102
  /,
152
103
  *,
153
- list_separator: str = DEFAULT_SEPARATOR,
104
+ list_separator: str = LIST_SEPARATOR,
154
105
  pair_separator: str = PAIR_SEPARATOR,
155
106
  brackets: Iterable[tuple[str, str]] | None = BRACKETS,
156
107
  mapping: Literal[False] = False,
@@ -160,7 +111,7 @@ def split_key_value_pairs(
160
111
  text: str,
161
112
  /,
162
113
  *,
163
- list_separator: str = DEFAULT_SEPARATOR,
114
+ list_separator: str = LIST_SEPARATOR,
164
115
  pair_separator: str = PAIR_SEPARATOR,
165
116
  brackets: Iterable[tuple[str, str]] | None = BRACKETS,
166
117
  mapping: bool = False,
@@ -169,7 +120,7 @@ def split_key_value_pairs(
169
120
  text: str,
170
121
  /,
171
122
  *,
172
- list_separator: str = DEFAULT_SEPARATOR,
123
+ list_separator: str = LIST_SEPARATOR,
173
124
  pair_separator: str = PAIR_SEPARATOR,
174
125
  brackets: Iterable[tuple[str, str]] | None = BRACKETS,
175
126
  mapping: bool = False,
@@ -217,7 +168,7 @@ class _SplitKeyValuePairsDuplicateKeysError(SplitKeyValuePairsError):
217
168
 
218
169
  @override
219
170
  def __str__(self) -> str:
220
- return f"Unable to split {self.text!r} into a mapping since there are duplicate keys; got {get_repr(self.counts)}"
171
+ return f"Unable to split {self.text!r} into a mapping since there are duplicate keys; got {repr_(self.counts)}"
221
172
 
222
173
 
223
174
  ##
@@ -228,7 +179,7 @@ def split_str(
228
179
  text: str,
229
180
  /,
230
181
  *,
231
- separator: str = DEFAULT_SEPARATOR,
182
+ separator: str = LIST_SEPARATOR,
232
183
  brackets: Iterable[tuple[str, str]] | None = None,
233
184
  n: Literal[1],
234
185
  ) -> tuple[str]: ...
@@ -237,7 +188,7 @@ def split_str(
237
188
  text: str,
238
189
  /,
239
190
  *,
240
- separator: str = DEFAULT_SEPARATOR,
191
+ separator: str = LIST_SEPARATOR,
241
192
  brackets: Iterable[tuple[str, str]] | None = None,
242
193
  n: Literal[2],
243
194
  ) -> tuple[str, str]: ...
@@ -246,7 +197,7 @@ def split_str(
246
197
  text: str,
247
198
  /,
248
199
  *,
249
- separator: str = DEFAULT_SEPARATOR,
200
+ separator: str = LIST_SEPARATOR,
250
201
  brackets: Iterable[tuple[str, str]] | None = None,
251
202
  n: Literal[3],
252
203
  ) -> tuple[str, str, str]: ...
@@ -255,7 +206,7 @@ def split_str(
255
206
  text: str,
256
207
  /,
257
208
  *,
258
- separator: str = DEFAULT_SEPARATOR,
209
+ separator: str = LIST_SEPARATOR,
259
210
  brackets: Iterable[tuple[str, str]] | None = None,
260
211
  n: Literal[4],
261
212
  ) -> tuple[str, str, str, str]: ...
@@ -264,7 +215,7 @@ def split_str(
264
215
  text: str,
265
216
  /,
266
217
  *,
267
- separator: str = DEFAULT_SEPARATOR,
218
+ separator: str = LIST_SEPARATOR,
268
219
  brackets: Iterable[tuple[str, str]] | None = None,
269
220
  n: Literal[5],
270
221
  ) -> tuple[str, str, str, str, str]: ...
@@ -273,7 +224,7 @@ def split_str(
273
224
  text: str,
274
225
  /,
275
226
  *,
276
- separator: str = DEFAULT_SEPARATOR,
227
+ separator: str = LIST_SEPARATOR,
277
228
  brackets: Iterable[tuple[str, str]] | None = None,
278
229
  n: int | None = None,
279
230
  ) -> tuple[str, ...]: ...
@@ -281,7 +232,7 @@ def split_str(
281
232
  text: str,
282
233
  /,
283
234
  *,
284
- separator: str = DEFAULT_SEPARATOR,
235
+ separator: str = LIST_SEPARATOR,
285
236
  brackets: Iterable[tuple[str, str]] | None = None,
286
237
  n: int | None = None,
287
238
  ) -> tuple[str, ...]:
@@ -306,7 +257,7 @@ def _split_str_brackets(
306
257
  brackets: Iterable[tuple[str, str]],
307
258
  /,
308
259
  *,
309
- separator: str = DEFAULT_SEPARATOR,
260
+ separator: str = LIST_SEPARATOR,
310
261
  ) -> list[str]:
311
262
  brackets = list(brackets)
312
263
  opens, closes = transpose(brackets)
@@ -397,7 +348,7 @@ class _SplitStrOpeningBracketUnmatchedError(SplitStrError):
397
348
 
398
349
 
399
350
  def join_strs(
400
- texts: Iterable[str], /, *, sort: bool = False, separator: str = DEFAULT_SEPARATOR
351
+ texts: Iterable[str], /, *, sort: bool = False, separator: str = LIST_SEPARATOR
401
352
  ) -> str:
402
353
  """Join a collection of strings, with a special provision for the empty list."""
403
354
  texts = list(texts)
@@ -410,7 +361,7 @@ def join_strs(
410
361
  return separator.join(texts)
411
362
 
412
363
 
413
- def _escape_separator(*, separator: str = DEFAULT_SEPARATOR) -> str:
364
+ def _escape_separator(*, separator: str = LIST_SEPARATOR) -> str:
414
365
  return f"\\{separator}"
415
366
 
416
367
 
@@ -451,15 +402,6 @@ def str_encode(obj: Any, /) -> bytes:
451
402
  ##
452
403
 
453
404
 
454
- def strip_and_dedent(text: str, /, *, trailing: bool = False) -> str:
455
- """Strip and dedent a string."""
456
- result = dedent(text.strip("\n")).strip("\n")
457
- return f"{result}\n" if trailing else result
458
-
459
-
460
- ##
461
-
462
-
463
405
  @overload
464
406
  def to_bool(bool_: MaybeCallableBoolLike, /) -> bool: ...
465
407
  @overload
@@ -504,20 +446,7 @@ def to_str(text: MaybeCallableStr | None | Sentinel, /) -> str | None | Sentinel
504
446
  ##
505
447
 
506
448
 
507
- def unique_str() -> str:
508
- """Generate at unique string."""
509
- now = time_ns()
510
- pid = getpid()
511
- ident = get_ident()
512
- key = str(uuid4()).replace("-", "")
513
- return f"{now}_{pid}_{ident}_{key}"
514
-
515
-
516
449
  __all__ = [
517
- "BRACKETS",
518
- "DEFAULT_SEPARATOR",
519
- "LIST_SEPARATOR",
520
- "PAIR_SEPARATOR",
521
450
  "ParseBoolError",
522
451
  "ParseNoneError",
523
452
  "SplitKeyValuePairsError",
@@ -525,16 +454,12 @@ __all__ = [
525
454
  "join_strs",
526
455
  "parse_bool",
527
456
  "parse_none",
528
- "pascal_case",
529
457
  "prompt_bool",
530
- "repr_encode",
531
458
  "secret_str",
532
- "snake_case",
459
+ "split_f_str_equals",
533
460
  "split_key_value_pairs",
534
461
  "split_str",
535
462
  "str_encode",
536
- "strip_and_dedent",
537
463
  "to_bool",
538
464
  "to_str",
539
- "unique_str",
540
465
  ]
utilities/throttle.py ADDED
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from functools import partial, wraps
6
+ from inspect import iscoroutinefunction
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, NoReturn, assert_never, cast, override
9
+
10
+ from whenever import ZonedDateTime
11
+
12
+ from utilities.constants import SECOND
13
+ from utilities.core import get_env, get_now_local, write_text
14
+ from utilities.functions import in_timedelta
15
+ from utilities.pathlib import to_path
16
+ from utilities.types import Duration, MaybeCallablePathLike, MaybeCoro
17
+
18
+ if TYPE_CHECKING:
19
+ from utilities.types import Coro
20
+
21
+
22
+ def throttle[F: Callable[..., MaybeCoro[None]]](
23
+ *,
24
+ on_try: bool = False,
25
+ duration: Duration = SECOND,
26
+ path: MaybeCallablePathLike = Path.cwd,
27
+ raiser: Callable[[], NoReturn] | None = None,
28
+ ) -> Callable[[F], F]:
29
+ """Throttle a function. On success by default, on try otherwise."""
30
+ return cast(
31
+ "Any",
32
+ partial(
33
+ _throttle_inner, on_try=on_try, duration=duration, path=path, raiser=raiser
34
+ ),
35
+ )
36
+
37
+
38
+ def _throttle_inner[F: Callable[..., MaybeCoro[None]]](
39
+ func: F,
40
+ /,
41
+ *,
42
+ on_try: bool = False,
43
+ duration: Duration = SECOND,
44
+ path: MaybeCallablePathLike = Path.cwd,
45
+ raiser: Callable[[], NoReturn] | None = None,
46
+ ) -> F:
47
+ match bool(iscoroutinefunction(func)), on_try:
48
+ case False, False:
49
+
50
+ @wraps(func)
51
+ def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
52
+ path_use = to_path(path)
53
+ if _is_throttle(path=path_use, duration=duration):
54
+ _try_raise(raiser=raiser)
55
+ else:
56
+ cast("Callable[..., None]", func)(*args, **kwargs)
57
+ _write_throttle(path=path_use)
58
+
59
+ return cast("Any", throttle_sync_on_pass)
60
+
61
+ case False, True:
62
+
63
+ @wraps(func)
64
+ def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
65
+ path_use = to_path(path)
66
+ if _is_throttle(path=path_use, duration=duration):
67
+ _try_raise(raiser=raiser)
68
+ else:
69
+ _write_throttle(path=path_use)
70
+ cast("Callable[..., None]", func)(*args, **kwargs)
71
+
72
+ return cast("Any", throttle_sync_on_try)
73
+
74
+ case True, False:
75
+
76
+ @wraps(func)
77
+ async def throttle_async_on_pass(*args: Any, **kwargs: Any) -> None:
78
+ path_use = to_path(path)
79
+ if _is_throttle(path=path_use, duration=duration):
80
+ _try_raise(raiser=raiser)
81
+ else:
82
+ await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
83
+ _write_throttle(path=path_use)
84
+
85
+ return cast("Any", throttle_async_on_pass)
86
+
87
+ case True, True:
88
+
89
+ @wraps(func)
90
+ async def throttle_async_on_try(*args: Any, **kwargs: Any) -> None:
91
+ path_use = to_path(path)
92
+ if _is_throttle(path=path_use, duration=duration):
93
+ _try_raise(raiser=raiser)
94
+ else:
95
+ _write_throttle(path=path_use)
96
+ await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
97
+
98
+ return cast("Any", throttle_async_on_try)
99
+
100
+ case never:
101
+ assert_never(never)
102
+
103
+
104
+ def _is_throttle(
105
+ *, path: MaybeCallablePathLike = Path.cwd, duration: Duration = SECOND
106
+ ) -> bool:
107
+ if get_env("THROTTLE", nullable=True):
108
+ return False
109
+ path = to_path(path)
110
+ if path.is_file():
111
+ text = path.read_text().rstrip("\n")
112
+ if text == "":
113
+ path.unlink(missing_ok=True)
114
+ return False
115
+ try:
116
+ last = ZonedDateTime.parse_iso(text)
117
+ except ValueError:
118
+ raise _ThrottleParseZonedDateTimeError(path=path, text=text) from None
119
+ threshold = get_now_local() - in_timedelta(duration)
120
+ return threshold <= last
121
+ if not path.exists():
122
+ return False
123
+ raise _ThrottleMarkerFileError(path=path)
124
+
125
+
126
+ def _try_raise(*, raiser: Callable[[], NoReturn] | None = None) -> None:
127
+ if raiser is not None:
128
+ raiser()
129
+
130
+
131
+ def _write_throttle(*, path: MaybeCallablePathLike = Path.cwd) -> None:
132
+ path_use = to_path(path)
133
+ write_text(path_use, get_now_local().format_iso(), overwrite=True)
134
+
135
+
136
+ @dataclass(kw_only=True, slots=True)
137
+ class ThrottleError(Exception): ...
138
+
139
+
140
+ @dataclass(kw_only=True, slots=True)
141
+ class _ThrottleParseZonedDateTimeError(ThrottleError):
142
+ path: Path
143
+ text: str
144
+
145
+ @override
146
+ def __str__(self) -> str:
147
+ return f"Unable to parse the contents {self.text!r} of {str(self.path)!r} to a ZonedDateTime"
148
+
149
+
150
+ @dataclass(kw_only=True, slots=True)
151
+ class _ThrottleMarkerFileError(ThrottleError):
152
+ path: Path
153
+
154
+ @override
155
+ def __str__(self) -> str:
156
+ return f"Invalid marker file {str(self.path)!r}"
157
+
158
+
159
+ __all__ = ["ThrottleError", "throttle"]
utilities/time.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING
5
+
6
+ from utilities.functions import in_seconds
7
+
8
+ if TYPE_CHECKING:
9
+ from utilities.types import Duration
10
+
11
+
12
+ def sleep(duration: Duration | None = None, /) -> None:
13
+ """Sleep which accepts durations."""
14
+ if duration is not None:
15
+ time.sleep(in_seconds(duration))
16
+
17
+
18
+ __all__ = ["sleep"]
utilities/timer.py CHANGED
@@ -3,12 +3,15 @@ from __future__ import annotations
3
3
  from operator import add, eq, ge, gt, le, lt, mul, ne, sub, truediv
4
4
  from typing import TYPE_CHECKING, Any, Self, override
5
5
 
6
- from utilities.whenever import get_now_local
6
+ from whenever import TimeDelta
7
+
8
+ from utilities.core import get_now_local
9
+ from utilities.functions import in_timedelta
7
10
 
8
11
  if TYPE_CHECKING:
9
12
  from collections.abc import Callable
10
13
 
11
- from whenever import TimeDelta, ZonedDateTime
14
+ from whenever import ZonedDateTime
12
15
 
13
16
 
14
17
  class Timer:
@@ -56,33 +59,33 @@ class Timer:
56
59
 
57
60
  @override
58
61
  def __repr__(self) -> str:
59
- return self.timedelta.format_common_iso()
62
+ return self.timedelta.format_iso()
60
63
 
61
64
  @override
62
65
  def __str__(self) -> str:
63
- return self.timedelta.format_common_iso()
66
+ return self.timedelta.format_iso()
64
67
 
65
68
  # comparison
66
69
 
67
70
  @override
68
71
  def __eq__(self, other: object) -> bool:
69
- return self._apply_op(eq, other)
72
+ return self._apply_op(eq, other, cast=True, type_error=False)
70
73
 
71
74
  def __ge__(self, other: Any) -> bool:
72
- return self._apply_op(ge, other)
75
+ return self._apply_op(ge, other, cast=True)
73
76
 
74
77
  def __gt__(self, other: Any) -> bool:
75
- return self._apply_op(gt, other)
78
+ return self._apply_op(gt, other, cast=True)
76
79
 
77
80
  def __le__(self, other: Any) -> bool:
78
- return self._apply_op(le, other)
81
+ return self._apply_op(le, other, cast=True)
79
82
 
80
83
  def __lt__(self, other: Any) -> bool:
81
- return self._apply_op(lt, other)
84
+ return self._apply_op(lt, other, cast=True)
82
85
 
83
86
  @override
84
87
  def __ne__(self, other: object) -> bool:
85
- return self._apply_op(ne, other)
88
+ return self._apply_op(ne, other, cast=True, type_error=True)
86
89
 
87
90
  # properties
88
91
 
@@ -94,10 +97,24 @@ class Timer:
94
97
 
95
98
  # private
96
99
 
97
- def _apply_op(self, op: Callable[[Any, Any], Any], other: Any, /) -> Any:
98
- if isinstance(other, Timer):
99
- return op(self.timedelta, other.timedelta)
100
- return op(self.timedelta, other)
100
+ def _apply_op(
101
+ self,
102
+ op: Callable[[Any, Any], Any],
103
+ other: Any,
104
+ /,
105
+ *,
106
+ cast: bool = False,
107
+ type_error: bool | None = None,
108
+ ) -> Any:
109
+ other_use = other.timedelta if isinstance(other, Timer) else other
110
+ if cast:
111
+ if isinstance(other_use, float | int | TimeDelta):
112
+ other_use = in_timedelta(other_use)
113
+ elif type_error is not None:
114
+ return type_error
115
+ else:
116
+ raise TypeError # pragma: no cover
117
+ return op(self.timedelta, other_use)
101
118
 
102
119
 
103
120
  __all__ = ["Timer"]