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.
- dycw_utilities-0.185.8.dist-info/METADATA +33 -0
- dycw_utilities-0.185.8.dist-info/RECORD +90 -0
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +17 -10
- utilities/asyncio.py +50 -72
- utilities/atools.py +9 -11
- utilities/cachetools.py +16 -11
- utilities/click.py +76 -19
- utilities/concurrent.py +1 -1
- utilities/constants.py +492 -0
- utilities/contextlib.py +23 -30
- utilities/contextvars.py +1 -23
- utilities/core.py +2581 -0
- utilities/dataclasses.py +16 -119
- utilities/docker.py +387 -0
- utilities/enum.py +1 -1
- utilities/errors.py +2 -16
- utilities/fastapi.py +5 -5
- utilities/fpdf2.py +2 -1
- utilities/functions.py +34 -265
- utilities/http.py +2 -3
- utilities/hypothesis.py +84 -29
- utilities/importlib.py +17 -1
- utilities/iterables.py +39 -575
- utilities/jinja2.py +145 -0
- utilities/jupyter.py +5 -3
- utilities/libcst.py +1 -1
- utilities/lightweight_charts.py +4 -6
- utilities/logging.py +24 -24
- utilities/math.py +1 -36
- utilities/more_itertools.py +4 -6
- utilities/numpy.py +2 -1
- utilities/operator.py +2 -2
- utilities/orjson.py +42 -43
- utilities/os.py +4 -147
- utilities/packaging.py +129 -0
- utilities/parse.py +35 -15
- utilities/pathlib.py +3 -120
- utilities/platform.py +8 -90
- utilities/polars.py +38 -32
- utilities/postgres.py +37 -33
- utilities/pottery.py +20 -18
- utilities/pqdm.py +3 -4
- utilities/psutil.py +2 -3
- utilities/pydantic.py +25 -0
- utilities/pydantic_settings.py +87 -16
- utilities/pydantic_settings_sops.py +16 -3
- utilities/pyinstrument.py +4 -4
- utilities/pytest.py +96 -125
- utilities/pytest_plugins/pytest_regressions.py +2 -2
- utilities/pytest_regressions.py +32 -11
- utilities/random.py +2 -8
- utilities/redis.py +98 -94
- utilities/reprlib.py +11 -118
- utilities/shellingham.py +66 -0
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +13 -12
- utilities/sqlalchemy.py +57 -30
- utilities/sqlalchemy_polars.py +16 -25
- utilities/subprocess.py +2590 -0
- utilities/tabulate.py +32 -0
- utilities/testbook.py +8 -8
- utilities/text.py +24 -99
- utilities/throttle.py +159 -0
- utilities/time.py +18 -0
- utilities/timer.py +31 -14
- utilities/traceback.py +16 -23
- utilities/types.py +42 -2
- utilities/typing.py +26 -14
- utilities/uuid.py +1 -1
- utilities/version.py +202 -45
- utilities/whenever.py +53 -150
- dycw_utilities-0.166.30.dist-info/METADATA +0 -41
- dycw_utilities-0.166.30.dist-info/RECORD +0 -98
- dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
- utilities/aeventkit.py +0 -388
- utilities/atomicwrites.py +0 -182
- utilities/cryptography.py +0 -41
- utilities/getpass.py +0 -8
- utilities/git.py +0 -19
- utilities/gzip.py +0 -31
- utilities/json.py +0 -70
- utilities/pickle.py +0 -25
- utilities/re.py +0 -156
- utilities/sentinel.py +0 -73
- utilities/socket.py +0 -8
- utilities/string.py +0 -20
- utilities/tempfile.py +0 -77
- utilities/typed_settings.py +0 -152
- utilities/tzdata.py +0 -11
- utilities/tzlocal.py +0 -28
- utilities/warnings.py +0 -65
- utilities/zipfile.py +0 -25
- 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.
|
|
9
|
-
from utilities.
|
|
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
|
|
14
|
+
from utilities.types import Duration, PathLike
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def build_notebook_tester(
|
|
18
|
-
path: PathLike, /, *, throttle:
|
|
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,
|
|
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, /, *,
|
|
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
|
|
45
|
-
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
|
|
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.
|
|
25
|
-
from utilities.
|
|
26
|
-
from utilities.
|
|
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
|
|
104
|
-
"""
|
|
105
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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.
|
|
62
|
+
return self.timedelta.format_iso()
|
|
60
63
|
|
|
61
64
|
@override
|
|
62
65
|
def __str__(self) -> str:
|
|
63
|
-
return self.timedelta.
|
|
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(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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"]
|