dycw-utilities 0.146.2__py3-none-any.whl → 0.178.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.
Potentially problematic release.
This version of dycw-utilities might be problematic. Click here for more details.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +129 -50
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +131 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/packaging.py +115 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +5 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +317 -153
- utilities/pottery.py +10 -86
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -51
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +100 -126
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +9 -4
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +59 -38
- utilities/types.py +68 -22
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +663 -178
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.146.2.dist-info/METADATA +0 -41
- dycw_utilities-0.146.2.dist-info/RECORD +0 -99
- dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
- dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- utilities/typed_settings.py +0 -144
utilities/docker.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
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.iterables import always_iterable
|
|
9
|
+
from utilities.logging import to_logger
|
|
10
|
+
from utilities.subprocess import (
|
|
11
|
+
MKTEMP_DIR_CMD,
|
|
12
|
+
maybe_sudo_cmd,
|
|
13
|
+
mkdir,
|
|
14
|
+
mkdir_cmd,
|
|
15
|
+
rm_cmd,
|
|
16
|
+
run,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Iterator
|
|
21
|
+
|
|
22
|
+
from utilities.types import (
|
|
23
|
+
LoggerLike,
|
|
24
|
+
MaybeIterable,
|
|
25
|
+
PathLike,
|
|
26
|
+
Retry,
|
|
27
|
+
StrStrMapping,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def docker_compose_down(
|
|
32
|
+
*,
|
|
33
|
+
files: MaybeIterable[PathLike] | None = None,
|
|
34
|
+
print: bool = False, # noqa: A002
|
|
35
|
+
print_stdout: bool = False,
|
|
36
|
+
print_stderr: bool = False,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Stop and remove containers."""
|
|
39
|
+
args = docker_compose_down_cmd(files=files) # pragma: no cover
|
|
40
|
+
run( # pragma: no cover
|
|
41
|
+
*args, print=print, print_stdout=print_stdout, print_stderr=print_stderr
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def docker_compose_down_cmd(
|
|
46
|
+
*, files: MaybeIterable[PathLike] | None = None
|
|
47
|
+
) -> list[str]:
|
|
48
|
+
"""Command to use 'docker compose down' to stop and remove containers."""
|
|
49
|
+
return _docker_compose_cmd("down", files=files)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def docker_compose_pull(
|
|
53
|
+
*,
|
|
54
|
+
files: MaybeIterable[PathLike] | None = None,
|
|
55
|
+
print: bool = False, # noqa: A002
|
|
56
|
+
print_stdout: bool = False,
|
|
57
|
+
print_stderr: bool = False,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Pull service images."""
|
|
60
|
+
args = docker_compose_pull_cmd(files=files) # pragma: no cover
|
|
61
|
+
run( # pragma: no cover
|
|
62
|
+
*args, print=print, print_stdout=print_stdout, print_stderr=print_stderr
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def docker_compose_pull_cmd(
|
|
67
|
+
*, files: MaybeIterable[PathLike] | None = None
|
|
68
|
+
) -> list[str]:
|
|
69
|
+
"""Command to use 'docker compose pull' to pull service images."""
|
|
70
|
+
return _docker_compose_cmd("pull", files=files)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def docker_compose_up(
|
|
74
|
+
*,
|
|
75
|
+
files: MaybeIterable[PathLike] | None = None,
|
|
76
|
+
detach: bool = True,
|
|
77
|
+
print: bool = False, # noqa: A002
|
|
78
|
+
print_stdout: bool = False,
|
|
79
|
+
print_stderr: bool = False,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Create and start containers."""
|
|
82
|
+
args = docker_compose_up_cmd(files=files, detach=detach) # pragma: no cover
|
|
83
|
+
run( # pragma: no cover
|
|
84
|
+
*args, print=print, print_stdout=print_stdout, print_stderr=print_stderr
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def docker_compose_up_cmd(
|
|
89
|
+
*, files: MaybeIterable[PathLike] | None = None, detach: bool = True
|
|
90
|
+
) -> list[str]:
|
|
91
|
+
"""Command to use 'docker compose up' to create and start containers."""
|
|
92
|
+
args: list[str] = []
|
|
93
|
+
if detach:
|
|
94
|
+
args.append("--detach")
|
|
95
|
+
return _docker_compose_cmd("up", *args, files=files)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _docker_compose_cmd(
|
|
99
|
+
cmd: str, /, *args: str, files: MaybeIterable[PathLike] | None = None
|
|
100
|
+
) -> list[str]:
|
|
101
|
+
all_args: list[str] = ["docker", "compose"]
|
|
102
|
+
if files is not None:
|
|
103
|
+
for file in always_iterable(files):
|
|
104
|
+
all_args.extend(["--file", str(file)])
|
|
105
|
+
return [*all_args, cmd, *args]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@overload
|
|
112
|
+
def docker_cp(
|
|
113
|
+
src: tuple[str, PathLike],
|
|
114
|
+
dest: PathLike,
|
|
115
|
+
/,
|
|
116
|
+
*,
|
|
117
|
+
sudo: bool = False,
|
|
118
|
+
logger: LoggerLike | None = None,
|
|
119
|
+
) -> None: ...
|
|
120
|
+
@overload
|
|
121
|
+
def docker_cp(
|
|
122
|
+
src: PathLike,
|
|
123
|
+
dest: tuple[str, PathLike],
|
|
124
|
+
/,
|
|
125
|
+
*,
|
|
126
|
+
sudo: bool = False,
|
|
127
|
+
logger: LoggerLike | None = None,
|
|
128
|
+
) -> None: ...
|
|
129
|
+
def docker_cp(
|
|
130
|
+
src: PathLike | tuple[str, PathLike],
|
|
131
|
+
dest: PathLike | tuple[str, PathLike],
|
|
132
|
+
/,
|
|
133
|
+
*,
|
|
134
|
+
sudo: bool = False,
|
|
135
|
+
logger: LoggerLike | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Copy between a container and the local file system."""
|
|
138
|
+
match src, dest: # skipif-ci
|
|
139
|
+
case Path() | str(), (str() as cont, Path() | str() as dest_path):
|
|
140
|
+
docker_exec(
|
|
141
|
+
cont, *maybe_sudo_cmd(*mkdir_cmd(dest_path, parent=True), sudo=sudo)
|
|
142
|
+
)
|
|
143
|
+
run(*maybe_sudo_cmd(*docker_cp_cmd(src, dest), sudo=sudo), logger=logger)
|
|
144
|
+
case (str(), Path() | str()), Path() | str():
|
|
145
|
+
mkdir(dest, parent=True, sudo=sudo)
|
|
146
|
+
run(*maybe_sudo_cmd(*docker_cp_cmd(src, dest), sudo=sudo), logger=logger)
|
|
147
|
+
case _: # pragma: no cover
|
|
148
|
+
raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@overload
|
|
152
|
+
def docker_cp_cmd(src: tuple[str, PathLike], dest: PathLike, /) -> list[str]: ...
|
|
153
|
+
@overload
|
|
154
|
+
def docker_cp_cmd(src: PathLike, dest: tuple[str, PathLike], /) -> list[str]: ...
|
|
155
|
+
def docker_cp_cmd(
|
|
156
|
+
src: PathLike | tuple[str, PathLike], dest: PathLike | tuple[str, PathLike], /
|
|
157
|
+
) -> list[str]:
|
|
158
|
+
"""Command to use 'docker cp' to copy between a container and the local file system."""
|
|
159
|
+
args: list[str] = ["docker", "cp"]
|
|
160
|
+
match src, dest:
|
|
161
|
+
case ((Path() | str()), (str() as cont, Path() | str() as path)):
|
|
162
|
+
return [*args, str(src), f"{cont}:{path}"]
|
|
163
|
+
case (str() as cont, (Path() | str()) as path), (Path() | str() as dest):
|
|
164
|
+
return [*args, f"{cont}:{path}", str(dest)]
|
|
165
|
+
case _: # pragma: no cover
|
|
166
|
+
raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@overload
|
|
173
|
+
def docker_exec(
|
|
174
|
+
container: str,
|
|
175
|
+
cmd: str,
|
|
176
|
+
/,
|
|
177
|
+
*args: str,
|
|
178
|
+
env: StrStrMapping | None = None,
|
|
179
|
+
user: str | None = None,
|
|
180
|
+
workdir: PathLike | None = None,
|
|
181
|
+
input: str | None = None,
|
|
182
|
+
print: bool = False,
|
|
183
|
+
print_stdout: bool = False,
|
|
184
|
+
print_stderr: bool = False,
|
|
185
|
+
return_: Literal[True],
|
|
186
|
+
return_stdout: bool = False,
|
|
187
|
+
return_stderr: bool = False,
|
|
188
|
+
retry: Retry | None = None,
|
|
189
|
+
logger: LoggerLike | None = None,
|
|
190
|
+
**env_kwargs: str,
|
|
191
|
+
) -> str: ...
|
|
192
|
+
@overload
|
|
193
|
+
def docker_exec(
|
|
194
|
+
container: str,
|
|
195
|
+
cmd: str,
|
|
196
|
+
/,
|
|
197
|
+
*args: str,
|
|
198
|
+
env: StrStrMapping | None = None,
|
|
199
|
+
user: str | None = None,
|
|
200
|
+
workdir: PathLike | None = None,
|
|
201
|
+
input: str | None = None,
|
|
202
|
+
print: bool = False,
|
|
203
|
+
print_stdout: bool = False,
|
|
204
|
+
print_stderr: bool = False,
|
|
205
|
+
return_: bool = False,
|
|
206
|
+
return_stdout: Literal[True],
|
|
207
|
+
return_stderr: bool = False,
|
|
208
|
+
retry: Retry | None = None,
|
|
209
|
+
logger: LoggerLike | None = None,
|
|
210
|
+
**env_kwargs: str,
|
|
211
|
+
) -> str: ...
|
|
212
|
+
@overload
|
|
213
|
+
def docker_exec(
|
|
214
|
+
container: str,
|
|
215
|
+
cmd: str,
|
|
216
|
+
/,
|
|
217
|
+
*args: str,
|
|
218
|
+
env: StrStrMapping | None = None,
|
|
219
|
+
user: str | None = None,
|
|
220
|
+
workdir: PathLike | None = None,
|
|
221
|
+
input: str | None = None,
|
|
222
|
+
print: bool = False,
|
|
223
|
+
print_stdout: bool = False,
|
|
224
|
+
print_stderr: bool = False,
|
|
225
|
+
return_: bool = False,
|
|
226
|
+
return_stdout: bool = False,
|
|
227
|
+
return_stderr: Literal[True],
|
|
228
|
+
retry: Retry | None = None,
|
|
229
|
+
logger: LoggerLike | None = None,
|
|
230
|
+
**env_kwargs: str,
|
|
231
|
+
) -> str: ...
|
|
232
|
+
@overload
|
|
233
|
+
def docker_exec(
|
|
234
|
+
container: str,
|
|
235
|
+
cmd: str,
|
|
236
|
+
/,
|
|
237
|
+
*args: str,
|
|
238
|
+
env: StrStrMapping | None = None,
|
|
239
|
+
user: str | None = None,
|
|
240
|
+
workdir: PathLike | None = None,
|
|
241
|
+
input: str | None = None,
|
|
242
|
+
print: bool = False,
|
|
243
|
+
print_stdout: bool = False,
|
|
244
|
+
print_stderr: bool = False,
|
|
245
|
+
return_: Literal[False] = False,
|
|
246
|
+
return_stdout: Literal[False] = False,
|
|
247
|
+
return_stderr: Literal[False] = False,
|
|
248
|
+
retry: Retry | None = None,
|
|
249
|
+
logger: LoggerLike | None = None,
|
|
250
|
+
**env_kwargs: str,
|
|
251
|
+
) -> None: ...
|
|
252
|
+
@overload
|
|
253
|
+
def docker_exec(
|
|
254
|
+
container: str,
|
|
255
|
+
cmd: str,
|
|
256
|
+
/,
|
|
257
|
+
*args: str,
|
|
258
|
+
env: StrStrMapping | None = None,
|
|
259
|
+
user: str | None = None,
|
|
260
|
+
workdir: PathLike | None = None,
|
|
261
|
+
input: str | None = None,
|
|
262
|
+
print: bool = False,
|
|
263
|
+
print_stdout: bool = False,
|
|
264
|
+
print_stderr: bool = False,
|
|
265
|
+
return_: bool = False,
|
|
266
|
+
return_stdout: bool = False,
|
|
267
|
+
return_stderr: bool = False,
|
|
268
|
+
retry: Retry | None = None,
|
|
269
|
+
logger: LoggerLike | None = None,
|
|
270
|
+
**env_kwargs: str,
|
|
271
|
+
) -> str | None: ...
|
|
272
|
+
def docker_exec(
|
|
273
|
+
container: str,
|
|
274
|
+
cmd: str,
|
|
275
|
+
/,
|
|
276
|
+
*args: str,
|
|
277
|
+
env: StrStrMapping | None = None,
|
|
278
|
+
user: str | None = None,
|
|
279
|
+
workdir: PathLike | None = None,
|
|
280
|
+
input: str | None = None, # noqa: A002
|
|
281
|
+
print: bool = False, # noqa: A002
|
|
282
|
+
print_stdout: bool = False,
|
|
283
|
+
print_stderr: bool = False,
|
|
284
|
+
return_: bool = False,
|
|
285
|
+
return_stdout: bool = False,
|
|
286
|
+
return_stderr: bool = False,
|
|
287
|
+
retry: Retry | None = None,
|
|
288
|
+
logger: LoggerLike | None = None,
|
|
289
|
+
**env_kwargs: str,
|
|
290
|
+
) -> str | None:
|
|
291
|
+
"""Execute a command in a container."""
|
|
292
|
+
run_cmd_and_args = docker_exec_cmd( # skipif-ci
|
|
293
|
+
container,
|
|
294
|
+
cmd,
|
|
295
|
+
*args,
|
|
296
|
+
env=env,
|
|
297
|
+
interactive=input is not None,
|
|
298
|
+
user=user,
|
|
299
|
+
workdir=workdir,
|
|
300
|
+
**env_kwargs,
|
|
301
|
+
)
|
|
302
|
+
return run( # skipif-ci
|
|
303
|
+
*run_cmd_and_args,
|
|
304
|
+
input=input,
|
|
305
|
+
print=print,
|
|
306
|
+
print_stdout=print_stdout,
|
|
307
|
+
print_stderr=print_stderr,
|
|
308
|
+
return_=return_,
|
|
309
|
+
return_stdout=return_stdout,
|
|
310
|
+
return_stderr=return_stderr,
|
|
311
|
+
retry=retry,
|
|
312
|
+
logger=logger,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def docker_exec_cmd(
|
|
317
|
+
container: str,
|
|
318
|
+
cmd: str,
|
|
319
|
+
/,
|
|
320
|
+
*args: str,
|
|
321
|
+
env: StrStrMapping | None = None,
|
|
322
|
+
interactive: bool = False,
|
|
323
|
+
user: str | None = None,
|
|
324
|
+
workdir: PathLike | None = None,
|
|
325
|
+
**env_kwargs: str,
|
|
326
|
+
) -> list[str]:
|
|
327
|
+
"""Command to use `docker exec` to execute a command in a container."""
|
|
328
|
+
all_args: list[str] = ["docker", "exec"]
|
|
329
|
+
mapping: dict[str, str] = ({} if env is None else dict(env)) | env_kwargs
|
|
330
|
+
for key, value in mapping.items():
|
|
331
|
+
all_args.extend(["--env", f"{key}={value}"])
|
|
332
|
+
if interactive:
|
|
333
|
+
all_args.append("--interactive")
|
|
334
|
+
if user is not None:
|
|
335
|
+
all_args.extend(["--user", user])
|
|
336
|
+
if workdir is not None:
|
|
337
|
+
all_args.extend(["--workdir", str(workdir)])
|
|
338
|
+
return [*all_args, container, cmd, *args]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
##
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@contextmanager
|
|
345
|
+
def yield_docker_temp_dir(
|
|
346
|
+
container: str,
|
|
347
|
+
/,
|
|
348
|
+
*,
|
|
349
|
+
user: str | None = None,
|
|
350
|
+
retry: Retry | None = None,
|
|
351
|
+
logger: LoggerLike | None = None,
|
|
352
|
+
keep: bool = False,
|
|
353
|
+
) -> Iterator[Path]:
|
|
354
|
+
"""Yield a temporary directory in a Docker container."""
|
|
355
|
+
path = Path( # skipif-ci
|
|
356
|
+
docker_exec(
|
|
357
|
+
container,
|
|
358
|
+
*MKTEMP_DIR_CMD,
|
|
359
|
+
user=user,
|
|
360
|
+
return_=True,
|
|
361
|
+
retry=retry,
|
|
362
|
+
logger=logger,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
try: # skipif-ci
|
|
366
|
+
yield path
|
|
367
|
+
finally: # skipif-ci
|
|
368
|
+
if keep:
|
|
369
|
+
if logger is not None:
|
|
370
|
+
to_logger(logger).info("Keeping temporary directory '%s'...", path)
|
|
371
|
+
else:
|
|
372
|
+
docker_exec(container, *rm_cmd(path), user=user, retry=retry, logger=logger)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
__all__ = [
|
|
376
|
+
"docker_compose_down",
|
|
377
|
+
"docker_compose_down_cmd",
|
|
378
|
+
"docker_compose_pull",
|
|
379
|
+
"docker_compose_pull_cmd",
|
|
380
|
+
"docker_compose_up",
|
|
381
|
+
"docker_compose_up_cmd",
|
|
382
|
+
"docker_cp",
|
|
383
|
+
"docker_cp_cmd",
|
|
384
|
+
"docker_exec",
|
|
385
|
+
"docker_exec_cmd",
|
|
386
|
+
"yield_docker_temp_dir",
|
|
387
|
+
]
|
utilities/enum.py
CHANGED
|
@@ -95,7 +95,7 @@ def parse_enum[E: Enum](
|
|
|
95
95
|
by_name=by_name.name,
|
|
96
96
|
by_value=by_value.name,
|
|
97
97
|
)
|
|
98
|
-
case
|
|
98
|
+
case never:
|
|
99
99
|
assert_never(never)
|
|
100
100
|
|
|
101
101
|
|
|
@@ -116,7 +116,7 @@ def _parse_enum_one[E: Enum](
|
|
|
116
116
|
names = [e.name for e in enum]
|
|
117
117
|
case "values":
|
|
118
118
|
names = [ensure_str(e.value) for e in enum]
|
|
119
|
-
case
|
|
119
|
+
case never:
|
|
120
120
|
assert_never(never)
|
|
121
121
|
try:
|
|
122
122
|
name = one_str(names, value, case_sensitive=case_sensitive)
|
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
|
@@ -13,7 +13,7 @@ from utilities.whenever import get_now_local
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from collections.abc import AsyncIterator
|
|
15
15
|
|
|
16
|
-
from utilities.types import Delta
|
|
16
|
+
from utilities.types import Delta, MaybeType
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
_TASKS: list[Task[None]] = []
|
|
@@ -35,14 +35,19 @@ class _PingerReceiverApp(FastAPI):
|
|
|
35
35
|
|
|
36
36
|
@enhanced_async_context_manager
|
|
37
37
|
async def yield_ping_receiver(
|
|
38
|
-
port: int,
|
|
38
|
+
port: int,
|
|
39
|
+
/,
|
|
40
|
+
*,
|
|
41
|
+
host: str = "localhost",
|
|
42
|
+
timeout: Delta | None = None,
|
|
43
|
+
error: MaybeType[BaseException] = TimeoutError,
|
|
39
44
|
) -> AsyncIterator[None]:
|
|
40
45
|
"""Yield the ping receiver."""
|
|
41
46
|
app = _PingerReceiverApp() # skipif-ci
|
|
42
47
|
server = Server(Config(app, host=host, port=port)) # skipif-ci
|
|
43
48
|
_TASKS.append(create_task(server.serve())) # skipif-ci
|
|
44
49
|
try: # skipif-ci
|
|
45
|
-
async with timeout_td(timeout):
|
|
50
|
+
async with timeout_td(timeout, error=error):
|
|
46
51
|
yield
|
|
47
52
|
finally: # skipif-ci
|
|
48
53
|
await server.shutdown()
|
utilities/fpdf2.py
CHANGED
|
@@ -6,7 +6,7 @@ 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,
|
|
9
|
+
from utilities.whenever import format_compact, get_now_local
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from collections.abc import Iterator
|
|
@@ -47,7 +47,7 @@ def yield_pdf(*, header: str | None = None) -> Iterator[_BasePDF]:
|
|
|
47
47
|
def footer(self) -> None:
|
|
48
48
|
self.set_y(-15)
|
|
49
49
|
self.set_font(family="Helvetica", style="I", size=8)
|
|
50
|
-
page_no, now = (self.page_no(), format_compact(
|
|
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,
|