dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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.

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