dycw-utilities 0.148.5__py3-none-any.whl → 0.174.12__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.174.12.dist-info/METADATA +41 -0
- dycw_utilities-0.174.12.dist-info/RECORD +104 -0
- dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
- {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
- utilities/__init__.py +1 -1
- utilities/{eventkit.py → aeventkit.py} +12 -11
- utilities/altair.py +7 -6
- utilities/asyncio.py +113 -64
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +145 -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 +292 -0
- utilities/enum.py +2 -2
- utilities/errors.py +1 -1
- 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 +360 -78
- utilities/inflect.py +1 -1
- utilities/iterables.py +12 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +74 -85
- 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/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +297 -0
- utilities/platform.py +5 -5
- utilities/polars.py +932 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +296 -174
- utilities/pottery.py +8 -73
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +155 -46
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +2 -3
- 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 +8 -3
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +864 -0
- utilities/tempfile.py +62 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +2 -2
- utilities/traceback.py +46 -36
- utilities/types.py +62 -23
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +661 -151
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.148.5.dist-info/METADATA +0 -41
- dycw_utilities-0.148.5.dist-info/RECORD +0 -95
- dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
- dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
- utilities/period.py +0 -237
- utilities/typed_settings.py +0 -144
utilities/dataclasses.py
CHANGED
|
@@ -6,11 +6,7 @@ from dataclasses import MISSING, dataclass, field, fields, replace
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Literal, assert_never, overload, override
|
|
7
7
|
|
|
8
8
|
from utilities.errors import ImpossibleCaseError
|
|
9
|
-
from utilities.functions import
|
|
10
|
-
get_class_name,
|
|
11
|
-
is_dataclass_class,
|
|
12
|
-
is_dataclass_instance,
|
|
13
|
-
)
|
|
9
|
+
from utilities.functions import get_class_name
|
|
14
10
|
from utilities.iterables import (
|
|
15
11
|
OneStrEmptyError,
|
|
16
12
|
OneStrNonUniqueError,
|
|
@@ -25,7 +21,7 @@ from utilities.parse import (
|
|
|
25
21
|
serialize_object,
|
|
26
22
|
)
|
|
27
23
|
from utilities.re import ExtractGroupError, extract_group
|
|
28
|
-
from utilities.sentinel import Sentinel, sentinel
|
|
24
|
+
from utilities.sentinel import Sentinel, is_sentinel, sentinel
|
|
29
25
|
from utilities.text import (
|
|
30
26
|
BRACKETS,
|
|
31
27
|
LIST_SEPARATOR,
|
|
@@ -34,8 +30,8 @@ from utilities.text import (
|
|
|
34
30
|
_SplitKeyValuePairsSplitError,
|
|
35
31
|
split_key_value_pairs,
|
|
36
32
|
)
|
|
37
|
-
from utilities.types import SupportsLT
|
|
38
|
-
from utilities.typing import get_type_hints
|
|
33
|
+
from utilities.types import MaybeType, SupportsLT
|
|
34
|
+
from utilities.typing import get_type_hints, is_dataclass_class, is_dataclass_instance
|
|
39
35
|
|
|
40
36
|
if TYPE_CHECKING:
|
|
41
37
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -214,7 +210,7 @@ def is_nullable_lt[T: SupportsLT](x: T | None, y: T | None, /) -> bool | None:
|
|
|
214
210
|
return True
|
|
215
211
|
case 0:
|
|
216
212
|
return None
|
|
217
|
-
case
|
|
213
|
+
case never:
|
|
218
214
|
assert_never(never)
|
|
219
215
|
|
|
220
216
|
|
|
@@ -275,8 +271,7 @@ def mapping_to_dataclass[T: Dataclass](
|
|
|
275
271
|
default = {
|
|
276
272
|
f.name
|
|
277
273
|
for f in fields_use
|
|
278
|
-
if (not
|
|
279
|
-
or (not isinstance(f.default_factory, Sentinel))
|
|
274
|
+
if (not is_sentinel(f.default)) or (not is_sentinel(f.default_factory))
|
|
280
275
|
}
|
|
281
276
|
have = set(field_names_to_values) | default
|
|
282
277
|
missing = {f.name for f in fields_use} - have
|
|
@@ -434,12 +429,10 @@ def replace_non_sentinel[T: Dataclass](
|
|
|
434
429
|
"""Replace attributes on a dataclass, filtering out sentinel values."""
|
|
435
430
|
if in_place:
|
|
436
431
|
for k, v in kwargs.items():
|
|
437
|
-
if not
|
|
432
|
+
if not is_sentinel(v):
|
|
438
433
|
setattr(obj, k, v)
|
|
439
434
|
return None
|
|
440
|
-
return replace(
|
|
441
|
-
obj, **{k: v for k, v in kwargs.items() if not isinstance(v, Sentinel)}
|
|
442
|
-
)
|
|
435
|
+
return replace(obj, **{k: v for k, v in kwargs.items() if not is_sentinel(v)})
|
|
443
436
|
|
|
444
437
|
|
|
445
438
|
##
|
|
@@ -520,7 +513,7 @@ def parse_dataclass[T: Dataclass](
|
|
|
520
513
|
)
|
|
521
514
|
case Mapping() as keys_to_serializes:
|
|
522
515
|
...
|
|
523
|
-
case
|
|
516
|
+
case never:
|
|
524
517
|
assert_never(never)
|
|
525
518
|
fields = list(
|
|
526
519
|
yield_fields(
|
|
@@ -833,7 +826,7 @@ def yield_fields(
|
|
|
833
826
|
warn_name_errors: bool = False,
|
|
834
827
|
) -> Iterator[_YieldFieldsClass[Any]]: ...
|
|
835
828
|
def yield_fields(
|
|
836
|
-
obj:
|
|
829
|
+
obj: MaybeType[Dataclass],
|
|
837
830
|
/,
|
|
838
831
|
*,
|
|
839
832
|
globalns: StrMapping | None = None,
|
|
@@ -912,17 +905,11 @@ class _YieldFieldsInstance[T]:
|
|
|
912
905
|
extra: Mapping[type[U], Callable[[U, U], bool]] | None = None,
|
|
913
906
|
) -> bool:
|
|
914
907
|
"""Check if the field value equals its default."""
|
|
915
|
-
if
|
|
916
|
-
self.default_factory, Sentinel
|
|
917
|
-
):
|
|
908
|
+
if is_sentinel(self.default) and is_sentinel(self.default_factory):
|
|
918
909
|
return False
|
|
919
|
-
if (not
|
|
920
|
-
self.default_factory, Sentinel
|
|
921
|
-
):
|
|
910
|
+
if (not is_sentinel(self.default)) and is_sentinel(self.default_factory):
|
|
922
911
|
expected = self.default
|
|
923
|
-
elif
|
|
924
|
-
not isinstance(self.default_factory, Sentinel)
|
|
925
|
-
):
|
|
912
|
+
elif is_sentinel(self.default) and (not is_sentinel(self.default_factory)):
|
|
926
913
|
expected = self.default_factory()
|
|
927
914
|
else: # pragma: no cover
|
|
928
915
|
raise ImpossibleCaseError(
|
|
@@ -1002,7 +989,7 @@ def _empty_error_str_core(
|
|
|
1002
989
|
return f"any field starting with {key!r}"
|
|
1003
990
|
case True, False:
|
|
1004
991
|
return f"any field starting with {key!r} (modulo case)"
|
|
1005
|
-
case
|
|
992
|
+
case never:
|
|
1006
993
|
assert_never(never)
|
|
1007
994
|
|
|
1008
995
|
|
|
@@ -1043,7 +1030,7 @@ def _non_unique_error_str_core(
|
|
|
1043
1030
|
head_msg = f"exactly one field starting with {key!r}"
|
|
1044
1031
|
case True, False:
|
|
1045
1032
|
head_msg = f"exactly one field starting with {key!r} (modulo case)"
|
|
1046
|
-
case
|
|
1033
|
+
case never:
|
|
1047
1034
|
assert_never(never)
|
|
1048
1035
|
return f"{head_msg}; got {first!r}, {second!r} and perhaps more"
|
|
1049
1036
|
|
utilities/docker.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
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:
|
|
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
|
+
path = Path( # skipif-ci
|
|
273
|
+
docker_exec(
|
|
274
|
+
container,
|
|
275
|
+
*MKTEMP_DIR_CMD,
|
|
276
|
+
user=user,
|
|
277
|
+
return_=True,
|
|
278
|
+
retry=retry,
|
|
279
|
+
logger=logger,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
try: # skipif-ci
|
|
283
|
+
yield path
|
|
284
|
+
finally: # skipif-ci
|
|
285
|
+
if keep:
|
|
286
|
+
if logger is not None:
|
|
287
|
+
to_logger(logger).info("Keeping temporary directory '%s'...", path)
|
|
288
|
+
else:
|
|
289
|
+
docker_exec(container, *rm_cmd(path), user=user, retry=retry, logger=logger)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
__all__ = ["docker_cp_cmd", "docker_exec", "docker_exec_cmd", "yield_docker_temp_dir"]
|
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
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,
|