dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__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.175.17.dist-info/METADATA +34 -0
- dycw_utilities-0.175.17.dist-info/RECORD +103 -0
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +14 -14
- utilities/asyncio.py +350 -819
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +77 -22
- utilities/cachetools.py +24 -29
- utilities/click.py +393 -237
- utilities/concurrent.py +8 -11
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +83 -118
- utilities/docker.py +293 -0
- utilities/enum.py +26 -23
- utilities/errors.py +17 -3
- utilities/fastapi.py +29 -65
- utilities/fpdf2.py +3 -3
- utilities/functions.py +169 -416
- utilities/functools.py +18 -19
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +738 -589
- utilities/importlib.py +17 -1
- utilities/inflect.py +25 -0
- utilities/iterables.py +194 -262
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +5 -9
- utilities/logging.py +345 -543
- utilities/math.py +18 -13
- utilities/memory_profiler.py +11 -15
- utilities/more_itertools.py +200 -131
- utilities/operator.py +33 -29
- utilities/optuna.py +6 -6
- utilities/orjson.py +272 -137
- utilities/os.py +61 -4
- utilities/parse.py +59 -61
- utilities/pathlib.py +281 -40
- utilities/permissions.py +298 -0
- utilities/pickle.py +2 -2
- utilities/platform.py +24 -5
- utilities/polars.py +1214 -430
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +113 -26
- utilities/pqdm.py +10 -11
- utilities/psutil.py +6 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -54
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +8 -10
- utilities/pytest.py +227 -121
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +13 -9
- utilities/re.py +58 -28
- utilities/redis.py +401 -550
- utilities/scipy.py +1 -1
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +36 -106
- utilities/sqlalchemy.py +502 -473
- utilities/sqlalchemy_polars.py +38 -94
- utilities/string.py +2 -3
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +86 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +37 -65
- utilities/traceback.py +158 -929
- utilities/types.py +146 -116
- utilities/typing.py +531 -71
- utilities/tzdata.py +1 -53
- utilities/tzlocal.py +6 -23
- utilities/uuid.py +43 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1776 -386
- utilities/zoneinfo.py +84 -22
- dycw_utilities-0.129.10.dist-info/METADATA +0 -241
- dycw_utilities-0.129.10.dist-info/RECORD +0 -96
- dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
- dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
- utilities/datetime.py +0 -1409
- utilities/eventkit.py +0 -402
- utilities/loguru.py +0 -144
- utilities/luigi.py +0 -228
- utilities/period.py +0 -324
- utilities/pyrsistent.py +0 -89
- utilities/python_dotenv.py +0 -105
- utilities/streamlit.py +0 -105
- utilities/sys.py +0 -87
- utilities/tenacity.py +0 -145
utilities/docker.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, overload
|
|
6
|
+
|
|
7
|
+
from utilities.errors import ImpossibleCaseError
|
|
8
|
+
from utilities.logging import to_logger
|
|
9
|
+
from utilities.subprocess import (
|
|
10
|
+
MKTEMP_DIR_CMD,
|
|
11
|
+
maybe_sudo_cmd,
|
|
12
|
+
mkdir,
|
|
13
|
+
mkdir_cmd,
|
|
14
|
+
rm_cmd,
|
|
15
|
+
run,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Iterator
|
|
20
|
+
|
|
21
|
+
from utilities.types import LoggerLike, PathLike, Retry, StrStrMapping
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@overload
|
|
25
|
+
def docker_cp(
|
|
26
|
+
src: tuple[str, PathLike],
|
|
27
|
+
dest: PathLike,
|
|
28
|
+
/,
|
|
29
|
+
*,
|
|
30
|
+
sudo: bool = False,
|
|
31
|
+
logger: LoggerLike | None = None,
|
|
32
|
+
) -> None: ...
|
|
33
|
+
@overload
|
|
34
|
+
def docker_cp(
|
|
35
|
+
src: PathLike,
|
|
36
|
+
dest: tuple[str, PathLike],
|
|
37
|
+
/,
|
|
38
|
+
*,
|
|
39
|
+
sudo: bool = False,
|
|
40
|
+
logger: LoggerLike | None = None,
|
|
41
|
+
) -> None: ...
|
|
42
|
+
def docker_cp(
|
|
43
|
+
src: PathLike | tuple[str, PathLike],
|
|
44
|
+
dest: PathLike | tuple[str, PathLike],
|
|
45
|
+
/,
|
|
46
|
+
*,
|
|
47
|
+
sudo: bool = False,
|
|
48
|
+
logger: LoggerLike | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
match src, dest: # skipif-ci
|
|
51
|
+
case Path() | str(), (str() as cont, Path() | str() as dest_path):
|
|
52
|
+
docker_exec(
|
|
53
|
+
cont, *maybe_sudo_cmd(*mkdir_cmd(dest_path, parent=True), sudo=sudo)
|
|
54
|
+
)
|
|
55
|
+
run(*docker_cp_cmd(src, dest, sudo=sudo), logger=logger)
|
|
56
|
+
case (str(), Path() | str()), Path() | str():
|
|
57
|
+
mkdir(dest, parent=True, sudo=sudo)
|
|
58
|
+
run(*docker_cp_cmd(src, dest, sudo=sudo), logger=logger)
|
|
59
|
+
case _: # pragma: no cover
|
|
60
|
+
raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def docker_cp_cmd(
|
|
65
|
+
src: tuple[str, PathLike], dest: PathLike, /, *, sudo: bool = False
|
|
66
|
+
) -> list[str]: ...
|
|
67
|
+
@overload
|
|
68
|
+
def docker_cp_cmd(
|
|
69
|
+
src: PathLike, dest: tuple[str, PathLike], /, *, sudo: bool = False
|
|
70
|
+
) -> list[str]: ...
|
|
71
|
+
def docker_cp_cmd(
|
|
72
|
+
src: PathLike | tuple[str, PathLike],
|
|
73
|
+
dest: PathLike | tuple[str, PathLike],
|
|
74
|
+
/,
|
|
75
|
+
*,
|
|
76
|
+
sudo: bool = False,
|
|
77
|
+
) -> list[str]:
|
|
78
|
+
match src, dest:
|
|
79
|
+
case (Path() | str()) as src_use, (
|
|
80
|
+
str() as dest_cont,
|
|
81
|
+
Path() | str() as dest_path,
|
|
82
|
+
):
|
|
83
|
+
dest_use = f"{dest_cont}:{dest_path}"
|
|
84
|
+
case (str() as src_cont, (Path() | str()) as src_path), (
|
|
85
|
+
Path() | str() as dest_use
|
|
86
|
+
):
|
|
87
|
+
src_use = f"{src_cont}:{src_path}"
|
|
88
|
+
case _: # pragma: no cover
|
|
89
|
+
raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
|
|
90
|
+
parts: list[str] = ["docker", "cp", str(src_use), str(dest_use)]
|
|
91
|
+
return maybe_sudo_cmd(*parts, sudo=sudo)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def docker_exec(
|
|
96
|
+
container: str,
|
|
97
|
+
cmd: str,
|
|
98
|
+
/,
|
|
99
|
+
*cmds_or_args: str,
|
|
100
|
+
env: StrStrMapping | None = None,
|
|
101
|
+
user: str | None = None,
|
|
102
|
+
workdir: PathLike | None = None,
|
|
103
|
+
input: str | None = None,
|
|
104
|
+
print: bool = False,
|
|
105
|
+
print_stdout: bool = False,
|
|
106
|
+
print_stderr: bool = False,
|
|
107
|
+
return_: Literal[True],
|
|
108
|
+
return_stdout: bool = False,
|
|
109
|
+
return_stderr: bool = False,
|
|
110
|
+
retry: Retry | None = None,
|
|
111
|
+
logger: LoggerLike | None = None,
|
|
112
|
+
**env_kwargs: str,
|
|
113
|
+
) -> str: ...
|
|
114
|
+
@overload
|
|
115
|
+
def docker_exec(
|
|
116
|
+
container: str,
|
|
117
|
+
cmd: str,
|
|
118
|
+
/,
|
|
119
|
+
*cmds_or_args: str,
|
|
120
|
+
env: StrStrMapping | None = None,
|
|
121
|
+
user: str | None = None,
|
|
122
|
+
workdir: PathLike | None = None,
|
|
123
|
+
input: str | None = None,
|
|
124
|
+
print: bool = False,
|
|
125
|
+
print_stdout: bool = False,
|
|
126
|
+
print_stderr: bool = False,
|
|
127
|
+
return_: bool = False,
|
|
128
|
+
return_stdout: Literal[True],
|
|
129
|
+
return_stderr: bool = False,
|
|
130
|
+
retry: Retry | None = None,
|
|
131
|
+
logger: LoggerLike | None = None,
|
|
132
|
+
**env_kwargs: str,
|
|
133
|
+
) -> str: ...
|
|
134
|
+
@overload
|
|
135
|
+
def docker_exec(
|
|
136
|
+
container: str,
|
|
137
|
+
cmd: str,
|
|
138
|
+
/,
|
|
139
|
+
*cmds_or_args: str,
|
|
140
|
+
env: StrStrMapping | None = None,
|
|
141
|
+
user: str | None = None,
|
|
142
|
+
workdir: PathLike | None = None,
|
|
143
|
+
input: str | None = None,
|
|
144
|
+
print: bool = False,
|
|
145
|
+
print_stdout: bool = False,
|
|
146
|
+
print_stderr: bool = False,
|
|
147
|
+
return_: bool = False,
|
|
148
|
+
return_stdout: bool = False,
|
|
149
|
+
return_stderr: Literal[True],
|
|
150
|
+
retry: Retry | None = None,
|
|
151
|
+
logger: LoggerLike | None = None,
|
|
152
|
+
**env_kwargs: str,
|
|
153
|
+
) -> str: ...
|
|
154
|
+
@overload
|
|
155
|
+
def docker_exec(
|
|
156
|
+
container: str,
|
|
157
|
+
cmd: str,
|
|
158
|
+
/,
|
|
159
|
+
*cmds_or_args: str,
|
|
160
|
+
env: StrStrMapping | None = None,
|
|
161
|
+
user: str | None = None,
|
|
162
|
+
workdir: PathLike | None = None,
|
|
163
|
+
input: str | None = None,
|
|
164
|
+
print: bool = False,
|
|
165
|
+
print_stdout: bool = False,
|
|
166
|
+
print_stderr: bool = False,
|
|
167
|
+
return_: Literal[False] = False,
|
|
168
|
+
return_stdout: Literal[False] = False,
|
|
169
|
+
return_stderr: Literal[False] = False,
|
|
170
|
+
retry: Retry | None = None,
|
|
171
|
+
logger: LoggerLike | None = None,
|
|
172
|
+
**env_kwargs: str,
|
|
173
|
+
) -> None: ...
|
|
174
|
+
@overload
|
|
175
|
+
def docker_exec(
|
|
176
|
+
container: str,
|
|
177
|
+
cmd: str,
|
|
178
|
+
/,
|
|
179
|
+
*cmds_or_args: str,
|
|
180
|
+
env: StrStrMapping | None = None,
|
|
181
|
+
user: str | None = None,
|
|
182
|
+
workdir: PathLike | None = None,
|
|
183
|
+
input: str | None = None,
|
|
184
|
+
print: bool = False,
|
|
185
|
+
print_stdout: bool = False,
|
|
186
|
+
print_stderr: bool = False,
|
|
187
|
+
return_: bool = False,
|
|
188
|
+
return_stdout: bool = False,
|
|
189
|
+
return_stderr: bool = False,
|
|
190
|
+
retry: Retry | None = None,
|
|
191
|
+
logger: LoggerLike | None = None,
|
|
192
|
+
**env_kwargs: str,
|
|
193
|
+
) -> str | None: ...
|
|
194
|
+
def docker_exec(
|
|
195
|
+
container: str,
|
|
196
|
+
cmd: str,
|
|
197
|
+
/,
|
|
198
|
+
*cmds_or_args: str,
|
|
199
|
+
env: StrStrMapping | None = None,
|
|
200
|
+
user: str | None = None,
|
|
201
|
+
workdir: PathLike | None = None,
|
|
202
|
+
input: str | None = None, # noqa: A002
|
|
203
|
+
print: bool = False, # noqa: A002
|
|
204
|
+
print_stdout: bool = False,
|
|
205
|
+
print_stderr: bool = False,
|
|
206
|
+
return_: bool = False,
|
|
207
|
+
return_stdout: bool = False,
|
|
208
|
+
return_stderr: bool = False,
|
|
209
|
+
retry: Retry | None = None,
|
|
210
|
+
logger: LoggerLike | None = None,
|
|
211
|
+
**env_kwargs: str,
|
|
212
|
+
) -> str | None:
|
|
213
|
+
cmd_and_args = docker_exec_cmd( # skipif-ci
|
|
214
|
+
container,
|
|
215
|
+
cmd,
|
|
216
|
+
*cmds_or_args,
|
|
217
|
+
env=env,
|
|
218
|
+
interactive=input is not None,
|
|
219
|
+
user=user,
|
|
220
|
+
workdir=workdir,
|
|
221
|
+
**env_kwargs,
|
|
222
|
+
)
|
|
223
|
+
return run( # skipif-ci
|
|
224
|
+
*cmd_and_args,
|
|
225
|
+
input=input,
|
|
226
|
+
print=print,
|
|
227
|
+
print_stdout=print_stdout,
|
|
228
|
+
print_stderr=print_stderr,
|
|
229
|
+
return_=return_,
|
|
230
|
+
return_stdout=return_stdout,
|
|
231
|
+
return_stderr=return_stderr,
|
|
232
|
+
retry=retry,
|
|
233
|
+
logger=logger,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def docker_exec_cmd(
|
|
238
|
+
container: str,
|
|
239
|
+
cmd: str,
|
|
240
|
+
/,
|
|
241
|
+
*cmds_or_args: str,
|
|
242
|
+
env: StrStrMapping | None = None,
|
|
243
|
+
interactive: bool = False,
|
|
244
|
+
user: str | None = None,
|
|
245
|
+
workdir: PathLike | None = None,
|
|
246
|
+
**env_kwargs: str,
|
|
247
|
+
) -> list[str]:
|
|
248
|
+
"""Build a command for `docker exec`."""
|
|
249
|
+
args: list[str] = ["docker", "exec"]
|
|
250
|
+
mapping: dict[str, str] = ({} if env is None else dict(env)) | env_kwargs
|
|
251
|
+
for key, value in mapping.items():
|
|
252
|
+
args.extend(["--env", f"{key}={value}"])
|
|
253
|
+
if interactive:
|
|
254
|
+
args.append("--interactive")
|
|
255
|
+
if user is not None:
|
|
256
|
+
args.extend(["--user", user])
|
|
257
|
+
if workdir is not None:
|
|
258
|
+
args.extend(["--workdir", str(workdir)])
|
|
259
|
+
return [*args, container, cmd, *cmds_or_args]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@contextmanager
|
|
263
|
+
def yield_docker_temp_dir(
|
|
264
|
+
container: str,
|
|
265
|
+
/,
|
|
266
|
+
*,
|
|
267
|
+
user: str | None = None,
|
|
268
|
+
retry: Retry | None = None,
|
|
269
|
+
logger: LoggerLike | None = None,
|
|
270
|
+
keep: bool = False,
|
|
271
|
+
) -> Iterator[Path]:
|
|
272
|
+
"""Yield a temporary directory in a Docker container."""
|
|
273
|
+
path = Path( # skipif-ci
|
|
274
|
+
docker_exec(
|
|
275
|
+
container,
|
|
276
|
+
*MKTEMP_DIR_CMD,
|
|
277
|
+
user=user,
|
|
278
|
+
return_=True,
|
|
279
|
+
retry=retry,
|
|
280
|
+
logger=logger,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
try: # skipif-ci
|
|
284
|
+
yield path
|
|
285
|
+
finally: # skipif-ci
|
|
286
|
+
if keep:
|
|
287
|
+
if logger is not None:
|
|
288
|
+
to_logger(logger).info("Keeping temporary directory '%s'...", path)
|
|
289
|
+
else:
|
|
290
|
+
docker_exec(container, *rm_cmd(path), user=user, retry=retry, logger=logger)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
__all__ = ["docker_cp_cmd", "docker_exec", "docker_exec_cmd", "yield_docker_temp_dir"]
|
utilities/enum.py
CHANGED
|
@@ -2,26 +2,29 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum, StrEnum
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, assert_never, overload, override
|
|
6
6
|
|
|
7
7
|
from utilities.functions import ensure_str
|
|
8
8
|
from utilities.iterables import OneStrEmptyError, OneStrNonUniqueError, one_str
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from utilities.types import EnumLike
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
##
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
@overload
|
|
15
|
-
def ensure_enum(
|
|
16
|
-
value: None, enum: type[
|
|
18
|
+
def ensure_enum[E: Enum](
|
|
19
|
+
value: None, enum: type[E], /, *, case_sensitive: bool = False
|
|
17
20
|
) -> None: ...
|
|
18
21
|
@overload
|
|
19
|
-
def ensure_enum(
|
|
20
|
-
value: EnumLike[
|
|
21
|
-
) ->
|
|
22
|
-
def ensure_enum(
|
|
23
|
-
value: EnumLike[
|
|
24
|
-
) ->
|
|
22
|
+
def ensure_enum[E: Enum](
|
|
23
|
+
value: EnumLike[E], enum: type[E], /, *, case_sensitive: bool = False
|
|
24
|
+
) -> E: ...
|
|
25
|
+
def ensure_enum[E: Enum](
|
|
26
|
+
value: EnumLike[E] | None, enum: type[E], /, *, case_sensitive: bool = False
|
|
27
|
+
) -> E | None:
|
|
25
28
|
"""Ensure the object is a member of the enum."""
|
|
26
29
|
if value is None:
|
|
27
30
|
return None
|
|
@@ -36,9 +39,9 @@ def ensure_enum(
|
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
@dataclass(kw_only=True, slots=True)
|
|
39
|
-
class EnsureEnumError(Exception
|
|
40
|
-
value: EnumLike[
|
|
41
|
-
enum: type[
|
|
42
|
+
class EnsureEnumError[E: Enum](Exception):
|
|
43
|
+
value: EnumLike[E]
|
|
44
|
+
enum: type[E]
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -58,9 +61,9 @@ class _EnsureEnumParseError(EnsureEnumError):
|
|
|
58
61
|
##
|
|
59
62
|
|
|
60
63
|
|
|
61
|
-
def parse_enum(
|
|
62
|
-
value: str, enum: type[
|
|
63
|
-
) ->
|
|
64
|
+
def parse_enum[E: Enum](
|
|
65
|
+
value: str, enum: type[E], /, *, case_sensitive: bool = False
|
|
66
|
+
) -> E:
|
|
64
67
|
"""Parse a string into the enum."""
|
|
65
68
|
by_name = _parse_enum_one(value, enum, "names", case_sensitive=case_sensitive)
|
|
66
69
|
if not issubclass(enum, StrEnum):
|
|
@@ -92,28 +95,28 @@ def parse_enum(
|
|
|
92
95
|
by_name=by_name.name,
|
|
93
96
|
by_value=by_value.name,
|
|
94
97
|
)
|
|
95
|
-
case
|
|
98
|
+
case never:
|
|
96
99
|
assert_never(never)
|
|
97
100
|
|
|
98
101
|
|
|
99
102
|
type _NamesOrValues = Literal["names", "values"]
|
|
100
103
|
|
|
101
104
|
|
|
102
|
-
def _parse_enum_one(
|
|
105
|
+
def _parse_enum_one[E: Enum](
|
|
103
106
|
value: str,
|
|
104
|
-
enum: type[
|
|
107
|
+
enum: type[E],
|
|
105
108
|
names_or_values: _NamesOrValues,
|
|
106
109
|
/,
|
|
107
110
|
*,
|
|
108
111
|
case_sensitive: bool = False,
|
|
109
|
-
) ->
|
|
112
|
+
) -> E | None:
|
|
110
113
|
"""Pair one aspect of the enums."""
|
|
111
114
|
match names_or_values:
|
|
112
115
|
case "names":
|
|
113
116
|
names = [e.name for e in enum]
|
|
114
117
|
case "values":
|
|
115
118
|
names = [ensure_str(e.value) for e in enum]
|
|
116
|
-
case
|
|
119
|
+
case never:
|
|
117
120
|
assert_never(never)
|
|
118
121
|
try:
|
|
119
122
|
name = one_str(names, value, case_sensitive=case_sensitive)
|
|
@@ -132,9 +135,9 @@ def _parse_enum_one(
|
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
@dataclass(kw_only=True, slots=True)
|
|
135
|
-
class ParseEnumError(Exception
|
|
138
|
+
class ParseEnumError[E: Enum](Exception):
|
|
136
139
|
value: str
|
|
137
|
-
enum: type[
|
|
140
|
+
enum: type[E]
|
|
138
141
|
|
|
139
142
|
|
|
140
143
|
@dataclass(kw_only=True, slots=True)
|
utilities/errors.py
CHANGED
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from typing import TYPE_CHECKING, assert_never, override
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from utilities.types import MaybeType
|
|
7
|
+
from utilities.types import ExceptionTypeLike, MaybeType
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -21,6 +21,20 @@ class ImpossibleCaseError(Exception):
|
|
|
21
21
|
##
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def is_instance_error(
|
|
25
|
+
error: BaseException, class_or_tuple: ExceptionTypeLike[Exception], /
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""Check if an instance relationship holds, allowing for groups."""
|
|
28
|
+
if isinstance(error, class_or_tuple):
|
|
29
|
+
return True
|
|
30
|
+
if not isinstance(error, BaseExceptionGroup):
|
|
31
|
+
return False
|
|
32
|
+
return any(is_instance_error(e, class_or_tuple) for e in error.exceptions)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
def repr_error(error: MaybeType[BaseException], /) -> str:
|
|
25
39
|
"""Get a string representation of an error."""
|
|
26
40
|
match error:
|
|
@@ -32,8 +46,8 @@ def repr_error(error: MaybeType[BaseException], /) -> str:
|
|
|
32
46
|
return f"{error_obj.__class__.__name__}({error_obj})"
|
|
33
47
|
case type() as error_cls:
|
|
34
48
|
return error_cls.__name__
|
|
35
|
-
case
|
|
49
|
+
case never:
|
|
36
50
|
assert_never(never)
|
|
37
51
|
|
|
38
52
|
|
|
39
|
-
__all__ = ["ImpossibleCaseError", "repr_error"]
|
|
53
|
+
__all__ = ["ImpossibleCaseError", "is_instance_error", "repr_error"]
|
utilities/fastapi.py
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from asyncio import Task, create_task
|
|
4
|
-
from
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Literal, Self, override
|
|
4
|
+
from typing import TYPE_CHECKING, Any, override
|
|
6
5
|
|
|
7
6
|
from fastapi import FastAPI
|
|
8
7
|
from uvicorn import Config, Server
|
|
9
8
|
|
|
10
|
-
from utilities.asyncio import
|
|
11
|
-
from utilities.
|
|
9
|
+
from utilities.asyncio import timeout_td
|
|
10
|
+
from utilities.contextlib import enhanced_async_context_manager
|
|
11
|
+
from utilities.whenever import get_now_local
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
15
|
|
|
16
|
-
from utilities.types import
|
|
16
|
+
from utilities.types import Delta, MaybeType
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
_TIMEOUT: Duration = SECOND
|
|
19
|
+
_TASKS: list[Task[None]] = []
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class _PingerReceiverApp(FastAPI):
|
|
@@ -29,64 +28,29 @@ class _PingerReceiverApp(FastAPI):
|
|
|
29
28
|
|
|
30
29
|
@self.get("/ping") # skipif-ci
|
|
31
30
|
def ping() -> str:
|
|
32
|
-
|
|
33
|
-
from utilities.whenever import serialize_zoned_datetime # skipif-ci
|
|
34
|
-
|
|
35
|
-
now = serialize_zoned_datetime(get_now_local()) # skipif-ci
|
|
36
|
-
return f"pong @ {now}" # skipif-ci
|
|
31
|
+
return f"pong @ {get_now_local()}" # skipif-ci
|
|
37
32
|
|
|
38
33
|
_ = ping # skipif-ci
|
|
39
34
|
|
|
40
35
|
|
|
41
|
-
@
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
self._server_task = create_task(self._server.serve())
|
|
63
|
-
return self # skipif-ci
|
|
64
|
-
|
|
65
|
-
@override
|
|
66
|
-
async def __aexit__(
|
|
67
|
-
self,
|
|
68
|
-
exc_type: type[BaseException] | None = None,
|
|
69
|
-
exc_value: BaseException | None = None,
|
|
70
|
-
traceback: TracebackType | None = None,
|
|
71
|
-
) -> None:
|
|
72
|
-
await super().__aexit__(exc_type, exc_value, traceback) # skipif-ci
|
|
73
|
-
await self._server.shutdown() # skipif-ci
|
|
74
|
-
|
|
75
|
-
@classmethod
|
|
76
|
-
async def ping(
|
|
77
|
-
cls, port: int, /, *, host: str = _LOCALHOST, timeout: Duration = _TIMEOUT
|
|
78
|
-
) -> str | Literal[False]:
|
|
79
|
-
"""Ping the receiver."""
|
|
80
|
-
from httpx import AsyncClient, ConnectError # skipif-ci
|
|
81
|
-
|
|
82
|
-
url = f"http://{host}:{port}/ping" # skipif-ci
|
|
83
|
-
timeout_use = datetime_duration_to_float(timeout) # skipif-ci
|
|
84
|
-
try: # skipif-ci
|
|
85
|
-
async with AsyncClient() as client:
|
|
86
|
-
response = await client.get(url, timeout=timeout_use)
|
|
87
|
-
except ConnectError: # skipif-ci
|
|
88
|
-
return False
|
|
89
|
-
return response.text if response.status_code == 200 else False # skipif-ci
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
__all__ = ["PingReceiver"]
|
|
36
|
+
@enhanced_async_context_manager
|
|
37
|
+
async def yield_ping_receiver(
|
|
38
|
+
port: int,
|
|
39
|
+
/,
|
|
40
|
+
*,
|
|
41
|
+
host: str = "localhost",
|
|
42
|
+
timeout: Delta | None = None,
|
|
43
|
+
error: MaybeType[BaseException] = TimeoutError,
|
|
44
|
+
) -> AsyncIterator[None]:
|
|
45
|
+
"""Yield the ping receiver."""
|
|
46
|
+
app = _PingerReceiverApp() # skipif-ci
|
|
47
|
+
server = Server(Config(app, host=host, port=port)) # skipif-ci
|
|
48
|
+
_TASKS.append(create_task(server.serve())) # skipif-ci
|
|
49
|
+
try: # skipif-ci
|
|
50
|
+
async with timeout_td(timeout, error=error):
|
|
51
|
+
yield
|
|
52
|
+
finally: # skipif-ci
|
|
53
|
+
await server.shutdown()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["yield_ping_receiver"]
|
utilities/fpdf2.py
CHANGED
|
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, override
|
|
|
6
6
|
from fpdf import FPDF
|
|
7
7
|
from fpdf.enums import XPos, YPos
|
|
8
8
|
|
|
9
|
+
from utilities.whenever import format_compact, get_now_local
|
|
10
|
+
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from collections.abc import Iterator
|
|
11
13
|
|
|
@@ -43,11 +45,9 @@ def yield_pdf(*, header: str | None = None) -> Iterator[_BasePDF]:
|
|
|
43
45
|
|
|
44
46
|
@override
|
|
45
47
|
def footer(self) -> None:
|
|
46
|
-
from utilities.tzlocal import get_now_local
|
|
47
|
-
|
|
48
48
|
self.set_y(-15)
|
|
49
49
|
self.set_font(family="Helvetica", style="I", size=8)
|
|
50
|
-
page_no, now = self.page_no(), get_now_local()
|
|
50
|
+
page_no, now = (self.page_no(), format_compact(get_now_local()))
|
|
51
51
|
text = f"page {page_no}/{{}}; {now}"
|
|
52
52
|
_ = self.cell(
|
|
53
53
|
w=0,
|