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.
Files changed (45) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/RECORD +43 -38
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +9 -4
  7. utilities/asyncio.py +10 -16
  8. utilities/cachetools.py +9 -6
  9. utilities/click.py +76 -20
  10. utilities/docker.py +293 -0
  11. utilities/functions.py +1 -1
  12. utilities/grp.py +28 -0
  13. utilities/hypothesis.py +38 -6
  14. utilities/importlib.py +17 -1
  15. utilities/jinja2.py +148 -0
  16. utilities/logging.py +7 -9
  17. utilities/orjson.py +18 -18
  18. utilities/os.py +38 -0
  19. utilities/parse.py +2 -2
  20. utilities/pathlib.py +18 -1
  21. utilities/permissions.py +298 -0
  22. utilities/platform.py +1 -1
  23. utilities/polars.py +4 -1
  24. utilities/postgres.py +28 -29
  25. utilities/pwd.py +28 -0
  26. utilities/pydantic.py +11 -0
  27. utilities/pydantic_settings.py +81 -8
  28. utilities/pydantic_settings_sops.py +13 -0
  29. utilities/pytest.py +60 -30
  30. utilities/pytest_regressions.py +26 -7
  31. utilities/shutil.py +25 -0
  32. utilities/sqlalchemy.py +15 -0
  33. utilities/subprocess.py +1572 -0
  34. utilities/tempfile.py +60 -1
  35. utilities/text.py +48 -32
  36. utilities/timer.py +2 -2
  37. utilities/traceback.py +1 -1
  38. utilities/types.py +5 -0
  39. utilities/typing.py +8 -2
  40. utilities/whenever.py +36 -5
  41. dycw_utilities-0.166.30.dist-info/METADATA +0 -41
  42. dycw_utilities-0.166.30.dist-info/WHEEL +0 -4
  43. dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
  44. utilities/aeventkit.py +0 -388
  45. 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.platform import IS_LINUX, IS_WINDOWS
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 datetime is out of range"),
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( # skipif-ci-and-windows
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( # skipif-ci-and-windows
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() # skipif-ci-and-windows
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 = ( # skipif-ci-and-windows
414
- self._time_handler.computeRollover(get_now_local().timestamp())
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: # skipif-ci-and-windows
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)) # skipif-ci-and-windows
424
+ return bool(self._time_handler.shouldRollover(record))
427
425
 
428
426
 
429
427
  def _compute_rollover_patterns(stem: str, suffix: str, /) -> _RolloverPatterns: