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.

Files changed (83) hide show
  1. dycw_utilities-0.174.12.dist-info/METADATA +41 -0
  2. dycw_utilities-0.174.12.dist-info/RECORD +104 -0
  3. dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
  5. utilities/__init__.py +1 -1
  6. utilities/{eventkit.py → aeventkit.py} +12 -11
  7. utilities/altair.py +7 -6
  8. utilities/asyncio.py +113 -64
  9. utilities/atomicwrites.py +1 -1
  10. utilities/atools.py +64 -4
  11. utilities/cachetools.py +9 -6
  12. utilities/click.py +145 -49
  13. utilities/concurrent.py +1 -1
  14. utilities/contextlib.py +4 -2
  15. utilities/contextvars.py +20 -1
  16. utilities/cryptography.py +3 -3
  17. utilities/dataclasses.py +15 -28
  18. utilities/docker.py +292 -0
  19. utilities/enum.py +2 -2
  20. utilities/errors.py +1 -1
  21. utilities/fastapi.py +8 -3
  22. utilities/fpdf2.py +2 -2
  23. utilities/functions.py +20 -297
  24. utilities/git.py +19 -0
  25. utilities/grp.py +28 -0
  26. utilities/hypothesis.py +360 -78
  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 +297 -0
  41. utilities/platform.py +5 -5
  42. utilities/polars.py +932 -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 +2 -3
  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 +864 -0
  67. utilities/tempfile.py +62 -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/period.py +0 -237
  83. 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 _ as never:
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 isinstance(f.default, Sentinel))
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 isinstance(v, Sentinel):
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 _ as never:
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: Dataclass | type[Dataclass],
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 isinstance(self.default, Sentinel) and isinstance(
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 isinstance(self.default, Sentinel)) and isinstance(
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 isinstance(self.default, Sentinel) and (
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 _ as never:
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 _ as never:
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 _ 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,