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.

Files changed (89) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +129 -50
  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 +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  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 +33 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +131 -93
  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/packaging.py +115 -0
  39. utilities/parse.py +2 -2
  40. utilities/pathlib.py +66 -34
  41. utilities/permissions.py +298 -0
  42. utilities/platform.py +5 -4
  43. utilities/polars.py +934 -420
  44. utilities/polars_ols.py +1 -1
  45. utilities/postgres.py +317 -153
  46. utilities/pottery.py +10 -86
  47. utilities/pqdm.py +3 -3
  48. utilities/pwd.py +28 -0
  49. utilities/pydantic.py +4 -51
  50. utilities/pydantic_settings.py +240 -0
  51. utilities/pydantic_settings_sops.py +76 -0
  52. utilities/pyinstrument.py +5 -5
  53. utilities/pytest.py +100 -126
  54. utilities/pytest_plugins/pytest_randomly.py +1 -1
  55. utilities/pytest_plugins/pytest_regressions.py +7 -3
  56. utilities/pytest_regressions.py +27 -8
  57. utilities/random.py +11 -6
  58. utilities/re.py +1 -1
  59. utilities/redis.py +101 -64
  60. utilities/sentinel.py +10 -0
  61. utilities/shelve.py +4 -1
  62. utilities/shutil.py +25 -0
  63. utilities/slack_sdk.py +9 -4
  64. utilities/sqlalchemy.py +422 -352
  65. utilities/sqlalchemy_polars.py +28 -52
  66. utilities/string.py +1 -1
  67. utilities/subprocess.py +1977 -0
  68. utilities/tempfile.py +112 -4
  69. utilities/testbook.py +50 -0
  70. utilities/text.py +174 -42
  71. utilities/throttle.py +158 -0
  72. utilities/timer.py +2 -2
  73. utilities/traceback.py +59 -38
  74. utilities/types.py +68 -22
  75. utilities/typing.py +479 -19
  76. utilities/uuid.py +42 -5
  77. utilities/version.py +27 -26
  78. utilities/whenever.py +663 -178
  79. utilities/zoneinfo.py +80 -22
  80. dycw_utilities-0.146.2.dist-info/METADATA +0 -41
  81. dycw_utilities-0.146.2.dist-info/RECORD +0 -99
  82. dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
  83. dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
  84. utilities/aiolimiter.py +0 -25
  85. utilities/eventkit.py +0 -388
  86. utilities/period.py +0 -237
  87. utilities/python_dotenv.py +0 -101
  88. utilities/streamlit.py +0 -105
  89. 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 _ 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
@@ -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 _ as never:
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, /, *, 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,