dycw-utilities 0.166.30__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.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/RECORD +43 -38
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +9 -4
- utilities/asyncio.py +10 -16
- utilities/cachetools.py +9 -6
- utilities/click.py +76 -20
- utilities/docker.py +293 -0
- utilities/functions.py +1 -1
- utilities/grp.py +28 -0
- utilities/hypothesis.py +38 -6
- utilities/importlib.py +17 -1
- utilities/jinja2.py +148 -0
- utilities/logging.py +7 -9
- utilities/orjson.py +18 -18
- utilities/os.py +38 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +18 -1
- utilities/permissions.py +298 -0
- utilities/platform.py +1 -1
- utilities/polars.py +4 -1
- utilities/postgres.py +28 -29
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +81 -8
- utilities/pydantic_settings_sops.py +13 -0
- utilities/pytest.py +60 -30
- utilities/pytest_regressions.py +26 -7
- utilities/shutil.py +25 -0
- utilities/sqlalchemy.py +15 -0
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +60 -1
- utilities/text.py +48 -32
- utilities/timer.py +2 -2
- utilities/traceback.py +1 -1
- utilities/types.py +5 -0
- utilities/typing.py +8 -2
- utilities/whenever.py +36 -5
- dycw_utilities-0.166.30.dist-info/METADATA +0 -41
- dycw_utilities-0.166.30.dist-info/WHEEL +0 -4
- dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
- utilities/aeventkit.py +0 -388
- utilities/typed_settings.py +0 -152
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/functions.py
CHANGED
|
@@ -693,7 +693,7 @@ def second[U](pair: tuple[Any, U], /) -> U:
|
|
|
693
693
|
|
|
694
694
|
def skip_if_optimize[**P](func: Callable[P, None], /) -> Callable[P, None]:
|
|
695
695
|
"""Skip a function if we are in the optimized mode."""
|
|
696
|
-
if __debug__:
|
|
696
|
+
if __debug__: # pragma: no cover
|
|
697
697
|
return func
|
|
698
698
|
|
|
699
699
|
@wraps(func)
|
utilities/grp.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import assert_never
|
|
4
|
+
|
|
5
|
+
from utilities.os import EFFECTIVE_GROUP_ID
|
|
6
|
+
from utilities.platform import SYSTEM
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_gid_name(gid: int, /) -> str | None:
|
|
10
|
+
"""Get the name of a group."""
|
|
11
|
+
match SYSTEM:
|
|
12
|
+
case "windows": # skipif-not-windows
|
|
13
|
+
return None
|
|
14
|
+
case "mac" | "linux":
|
|
15
|
+
from grp import getgrgid
|
|
16
|
+
|
|
17
|
+
return getgrgid(gid).gr_name
|
|
18
|
+
case never:
|
|
19
|
+
assert_never(never)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ROOT_GROUP_NAME = get_gid_name(0)
|
|
23
|
+
EFFECTIVE_GROUP_NAME = (
|
|
24
|
+
None if EFFECTIVE_GROUP_ID is None else get_gid_name(EFFECTIVE_GROUP_ID)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["EFFECTIVE_GROUP_NAME", "ROOT_GROUP_NAME", "get_gid_name"]
|
utilities/hypothesis.py
CHANGED
|
@@ -77,7 +77,8 @@ from utilities.math import (
|
|
|
77
77
|
)
|
|
78
78
|
from utilities.os import get_env_var
|
|
79
79
|
from utilities.pathlib import module_path, temp_cwd
|
|
80
|
-
from utilities.
|
|
80
|
+
from utilities.permissions import Permissions
|
|
81
|
+
from utilities.platform import IS_LINUX
|
|
81
82
|
from utilities.sentinel import Sentinel, is_sentinel, sentinel
|
|
82
83
|
from utilities.tempfile import TEMP_DIR, TemporaryDirectory
|
|
83
84
|
from utilities.version import Version
|
|
@@ -864,6 +865,38 @@ def _path_parts(draw: DrawFn, /) -> str:
|
|
|
864
865
|
##
|
|
865
866
|
|
|
866
867
|
|
|
868
|
+
@composite
|
|
869
|
+
def permissions(
|
|
870
|
+
draw: DrawFn,
|
|
871
|
+
/,
|
|
872
|
+
*,
|
|
873
|
+
user_read: MaybeSearchStrategy[bool | None] = None,
|
|
874
|
+
user_write: MaybeSearchStrategy[bool | None] = None,
|
|
875
|
+
user_execute: MaybeSearchStrategy[bool | None] = None,
|
|
876
|
+
group_read: MaybeSearchStrategy[bool | None] = None,
|
|
877
|
+
group_write: MaybeSearchStrategy[bool | None] = None,
|
|
878
|
+
group_execute: MaybeSearchStrategy[bool | None] = None,
|
|
879
|
+
others_read: MaybeSearchStrategy[bool | None] = None,
|
|
880
|
+
others_write: MaybeSearchStrategy[bool | None] = None,
|
|
881
|
+
others_execute: MaybeSearchStrategy[bool | None] = None,
|
|
882
|
+
) -> Permissions:
|
|
883
|
+
"""Strategy for generating `Permissions`."""
|
|
884
|
+
return Permissions(
|
|
885
|
+
user_read=draw2(draw, user_read, booleans()),
|
|
886
|
+
user_write=draw2(draw, user_write, booleans()),
|
|
887
|
+
user_execute=draw2(draw, user_execute, booleans()),
|
|
888
|
+
group_read=draw2(draw, group_read, booleans()),
|
|
889
|
+
group_write=draw2(draw, group_write, booleans()),
|
|
890
|
+
group_execute=draw2(draw, group_execute, booleans()),
|
|
891
|
+
others_read=draw2(draw, others_read, booleans()),
|
|
892
|
+
others_write=draw2(draw, others_write, booleans()),
|
|
893
|
+
others_execute=draw2(draw, others_execute, booleans()),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
##
|
|
898
|
+
|
|
899
|
+
|
|
867
900
|
@composite
|
|
868
901
|
def plain_date_times(
|
|
869
902
|
draw: DrawFn,
|
|
@@ -1007,7 +1040,7 @@ def setup_hypothesis_profiles(
|
|
|
1007
1040
|
assert_never(never)
|
|
1008
1041
|
|
|
1009
1042
|
phases = {Phase.explicit, Phase.reuse, Phase.generate, Phase.target}
|
|
1010
|
-
if "HYPOTHESIS_NO_SHRINK" not in environ:
|
|
1043
|
+
if "HYPOTHESIS_NO_SHRINK" not in environ: # pragma: no cover
|
|
1011
1044
|
phases.add(Phase.shrink)
|
|
1012
1045
|
for profile in Profile:
|
|
1013
1046
|
try:
|
|
@@ -1128,9 +1161,7 @@ def temp_dirs(draw: DrawFn, /) -> TemporaryDirectory:
|
|
|
1128
1161
|
"""Search strategy for temporary directories."""
|
|
1129
1162
|
_TEMP_DIR_HYPOTHESIS.mkdir(exist_ok=True)
|
|
1130
1163
|
uuid = draw(uuids())
|
|
1131
|
-
return TemporaryDirectory(
|
|
1132
|
-
prefix=f"{uuid}__", dir=_TEMP_DIR_HYPOTHESIS, ignore_cleanup_errors=IS_WINDOWS
|
|
1133
|
-
)
|
|
1164
|
+
return TemporaryDirectory(prefix=f"{uuid}__", dir=_TEMP_DIR_HYPOTHESIS)
|
|
1134
1165
|
|
|
1135
1166
|
|
|
1136
1167
|
##
|
|
@@ -1570,7 +1601,7 @@ def zoned_date_times(
|
|
|
1570
1601
|
with (
|
|
1571
1602
|
assume_does_not_raise(RepeatedTime),
|
|
1572
1603
|
assume_does_not_raise(SkippedTime),
|
|
1573
|
-
assume_does_not_raise(ValueError, match=r"Resulting
|
|
1604
|
+
assume_does_not_raise(ValueError, match=r"Resulting time is out of range"),
|
|
1574
1605
|
):
|
|
1575
1606
|
zoned = plain.assume_tz(time_zone_.key, disambiguate="raise")
|
|
1576
1607
|
with assume_does_not_raise(OverflowError, match=r"date value out of range"):
|
|
@@ -1613,6 +1644,7 @@ __all__ = [
|
|
|
1613
1644
|
"numbers",
|
|
1614
1645
|
"pairs",
|
|
1615
1646
|
"paths",
|
|
1647
|
+
"permissions",
|
|
1616
1648
|
"plain_date_times",
|
|
1617
1649
|
"py_datetimes",
|
|
1618
1650
|
"quadruples",
|
utilities/importlib.py
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib.resources
|
|
3
4
|
from importlib import import_module
|
|
4
5
|
from importlib.util import find_spec
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from utilities.errors import ImpossibleCaseError
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from importlib.resources import Anchor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def files(*, anchor: Anchor | None = None) -> Path:
|
|
16
|
+
"""Get the path for an anchor."""
|
|
17
|
+
path = importlib.resources.files(anchor)
|
|
18
|
+
if isinstance(path, Path):
|
|
19
|
+
return path
|
|
20
|
+
raise ImpossibleCaseError(case=[f"{path=}"]) # pragma: no cover
|
|
5
21
|
|
|
6
22
|
|
|
7
23
|
def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
|
|
@@ -15,4 +31,4 @@ def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
|
|
|
15
31
|
return hasattr(mod, name)
|
|
16
32
|
|
|
17
33
|
|
|
18
|
-
__all__ = ["is_valid_import"]
|
|
34
|
+
__all__ = ["files", "is_valid_import"]
|
utilities/jinja2.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, assert_never, override
|
|
5
|
+
|
|
6
|
+
from jinja2 import BaseLoader, BytecodeCache, Environment, FileSystemLoader, Undefined
|
|
7
|
+
from jinja2.defaults import (
|
|
8
|
+
BLOCK_END_STRING,
|
|
9
|
+
BLOCK_START_STRING,
|
|
10
|
+
COMMENT_END_STRING,
|
|
11
|
+
COMMENT_START_STRING,
|
|
12
|
+
KEEP_TRAILING_NEWLINE,
|
|
13
|
+
LINE_COMMENT_PREFIX,
|
|
14
|
+
LINE_STATEMENT_PREFIX,
|
|
15
|
+
LSTRIP_BLOCKS,
|
|
16
|
+
NEWLINE_SEQUENCE,
|
|
17
|
+
TRIM_BLOCKS,
|
|
18
|
+
VARIABLE_END_STRING,
|
|
19
|
+
VARIABLE_START_STRING,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from utilities.atomicwrites import writer
|
|
23
|
+
from utilities.text import kebab_case, pascal_case, snake_case
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Callable, Sequence
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from jinja2.ext import Extension
|
|
30
|
+
|
|
31
|
+
from utilities.types import StrMapping
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EnhancedEnvironment(Environment):
|
|
35
|
+
"""Environment with enhanced features."""
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
block_start_string: str = BLOCK_START_STRING,
|
|
41
|
+
block_end_string: str = BLOCK_END_STRING,
|
|
42
|
+
variable_start_string: str = VARIABLE_START_STRING,
|
|
43
|
+
variable_end_string: str = VARIABLE_END_STRING,
|
|
44
|
+
comment_start_string: str = COMMENT_START_STRING,
|
|
45
|
+
comment_end_string: str = COMMENT_END_STRING,
|
|
46
|
+
line_statement_prefix: str | None = LINE_STATEMENT_PREFIX,
|
|
47
|
+
line_comment_prefix: str | None = LINE_COMMENT_PREFIX,
|
|
48
|
+
trim_blocks: bool = TRIM_BLOCKS,
|
|
49
|
+
lstrip_blocks: bool = LSTRIP_BLOCKS,
|
|
50
|
+
newline_sequence: Literal["\n", "\r\n", "\r"] = NEWLINE_SEQUENCE,
|
|
51
|
+
keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
|
|
52
|
+
extensions: Sequence[str | type[Extension]] = (),
|
|
53
|
+
optimized: bool = True,
|
|
54
|
+
undefined: type[Undefined] = Undefined,
|
|
55
|
+
finalize: Callable[..., Any] | None = None,
|
|
56
|
+
autoescape: bool | Callable[[str | None], bool] = False,
|
|
57
|
+
loader: BaseLoader | None = None,
|
|
58
|
+
cache_size: int = 400,
|
|
59
|
+
auto_reload: bool = True,
|
|
60
|
+
bytecode_cache: BytecodeCache | None = None,
|
|
61
|
+
enable_async: bool = False,
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__(
|
|
64
|
+
block_start_string,
|
|
65
|
+
block_end_string,
|
|
66
|
+
variable_start_string,
|
|
67
|
+
variable_end_string,
|
|
68
|
+
comment_start_string,
|
|
69
|
+
comment_end_string,
|
|
70
|
+
line_statement_prefix,
|
|
71
|
+
line_comment_prefix,
|
|
72
|
+
trim_blocks,
|
|
73
|
+
lstrip_blocks,
|
|
74
|
+
newline_sequence,
|
|
75
|
+
keep_trailing_newline,
|
|
76
|
+
extensions,
|
|
77
|
+
optimized,
|
|
78
|
+
undefined,
|
|
79
|
+
finalize,
|
|
80
|
+
autoescape,
|
|
81
|
+
loader,
|
|
82
|
+
cache_size,
|
|
83
|
+
auto_reload,
|
|
84
|
+
bytecode_cache,
|
|
85
|
+
enable_async,
|
|
86
|
+
)
|
|
87
|
+
self.filters["kebab"] = kebab_case
|
|
88
|
+
self.filters["pascal"] = pascal_case
|
|
89
|
+
self.filters["snake"] = snake_case
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
93
|
+
class TemplateJob:
|
|
94
|
+
"""A template with an associated rendering job."""
|
|
95
|
+
|
|
96
|
+
template: Path
|
|
97
|
+
kwargs: StrMapping
|
|
98
|
+
target: Path
|
|
99
|
+
mode: Literal["write", "append"] = "write"
|
|
100
|
+
|
|
101
|
+
def __post_init__(self) -> None:
|
|
102
|
+
if not self.template.exists():
|
|
103
|
+
raise _TemplateJobTemplateDoesNotExistError(path=self.template)
|
|
104
|
+
if (self.mode == "append") and not self.target.exists():
|
|
105
|
+
raise _TemplateJobTargetDoesNotExistError(path=self.template)
|
|
106
|
+
|
|
107
|
+
def run(self) -> None:
|
|
108
|
+
"""Run the job."""
|
|
109
|
+
match self.mode:
|
|
110
|
+
case "write":
|
|
111
|
+
with writer(self.target, overwrite=True) as temp:
|
|
112
|
+
_ = temp.write_text(self.rendered)
|
|
113
|
+
case "append":
|
|
114
|
+
with self.target.open(mode="a") as fh:
|
|
115
|
+
_ = fh.write(self.rendered)
|
|
116
|
+
case never:
|
|
117
|
+
assert_never(never)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def rendered(self) -> str:
|
|
121
|
+
"""The template, rendered."""
|
|
122
|
+
env = EnhancedEnvironment(loader=FileSystemLoader(self.template.parent))
|
|
123
|
+
return env.get_template(self.template.name).render(self.kwargs)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(kw_only=True, slots=True)
|
|
127
|
+
class TemplateJobError(Exception): ...
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(kw_only=True, slots=True)
|
|
131
|
+
class _TemplateJobTemplateDoesNotExistError(TemplateJobError):
|
|
132
|
+
path: Path
|
|
133
|
+
|
|
134
|
+
@override
|
|
135
|
+
def __str__(self) -> str:
|
|
136
|
+
return f"Template {str(self.path)!r} does not exist"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(kw_only=True, slots=True)
|
|
140
|
+
class _TemplateJobTargetDoesNotExistError(TemplateJobError):
|
|
141
|
+
path: Path
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
def __str__(self) -> str:
|
|
145
|
+
return f"Target {str(self.path)!r} does not exist"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = ["EnhancedEnvironment", "TemplateJob", "TemplateJobError"]
|
utilities/logging.py
CHANGED
|
@@ -387,9 +387,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
|
|
|
387
387
|
def emit(self, record: LogRecord) -> None:
|
|
388
388
|
try:
|
|
389
389
|
if (self._backup_count is not None) and self._should_rollover(record):
|
|
390
|
-
self._do_rollover(
|
|
391
|
-
backup_count=self._backup_count
|
|
392
|
-
)
|
|
390
|
+
self._do_rollover(backup_count=self._backup_count)
|
|
393
391
|
FileHandler.emit(self, record)
|
|
394
392
|
except Exception: # noqa: BLE001 # pragma: no cover
|
|
395
393
|
self.handleError(record)
|
|
@@ -399,23 +397,23 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
|
|
|
399
397
|
self.stream.close()
|
|
400
398
|
self.stream = None
|
|
401
399
|
|
|
402
|
-
actions = _compute_rollover_actions(
|
|
400
|
+
actions = _compute_rollover_actions(
|
|
403
401
|
self._directory,
|
|
404
402
|
self._stem,
|
|
405
403
|
self._suffix,
|
|
406
404
|
patterns=self._patterns,
|
|
407
405
|
backup_count=backup_count,
|
|
408
406
|
)
|
|
409
|
-
actions.do()
|
|
407
|
+
actions.do()
|
|
410
408
|
|
|
411
409
|
if not self.delay: # pragma: no cover
|
|
412
410
|
self.stream = self._open()
|
|
413
|
-
self._time_handler.rolloverAt = (
|
|
414
|
-
|
|
411
|
+
self._time_handler.rolloverAt = self._time_handler.computeRollover(
|
|
412
|
+
get_now_local().timestamp()
|
|
415
413
|
)
|
|
416
414
|
|
|
417
415
|
def _should_rollover(self, record: LogRecord, /) -> bool:
|
|
418
|
-
if self._max_bytes is not None:
|
|
416
|
+
if self._max_bytes is not None:
|
|
419
417
|
try:
|
|
420
418
|
size = self._filename.stat().st_size
|
|
421
419
|
except FileNotFoundError:
|
|
@@ -423,7 +421,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
|
|
|
423
421
|
else:
|
|
424
422
|
if size >= self._max_bytes:
|
|
425
423
|
return True
|
|
426
|
-
return bool(self._time_handler.shouldRollover(record))
|
|
424
|
+
return bool(self._time_handler.shouldRollover(record))
|
|
427
425
|
|
|
428
426
|
|
|
429
427
|
def _compute_rollover_patterns(stem: str, suffix: str, /) -> _RolloverPatterns:
|