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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.166.36
3
+ Version: 0.167.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -12,7 +12,7 @@ Provides-Extra: logging
12
12
  Requires-Dist: coloredlogs<15.1,>=15.0.1; extra == 'logging'
13
13
  Provides-Extra: test
14
14
  Requires-Dist: dycw-pytest-only<2.2,>=2.1.1; extra == 'test'
15
- Requires-Dist: hypothesis<6.140,>=6.139.1; extra == 'test'
15
+ Requires-Dist: hypothesis<6.140,>=6.139.2; extra == 'test'
16
16
  Requires-Dist: pytest-asyncio<1.3,>=1.2.0; extra == 'test'
17
17
  Requires-Dist: pytest-cov<7.1,>=7.0.0; extra == 'test'
18
18
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=VioZbifSCOm0EkGF27DgQtAvEThNI6_JUJQ3jWhqrYI,61
1
+ utilities/__init__.py,sha256=dgiGa6KFuwS9sLQQWHYgtwuRxby-Kwzx_cLKTNpUlIc,60
2
2
  utilities/aeventkit.py,sha256=ddoleSwW9zdc2tjX5Ge0pMKtYwV_JMxhHYOxnWX2AGM,12609
3
3
  utilities/altair.py,sha256=nHdpWt8ZwdUwRQN970MvHd5bRWokNqzHcZQEdSHKRuE,9033
4
4
  utilities/asyncio.py,sha256=60l1IwjnRGeaVphAFiwDIHyfKoZYKY-XGpptUxGiU-M,17034
@@ -12,6 +12,7 @@ utilities/contextvars.py,sha256=J8OhC7jqozAGYOCe2KUWysbPXNGe5JYz3HfaY_mIs08,883
12
12
  utilities/cryptography.py,sha256=5PFrzsNUGHay91dFgYnDKwYprXxahrBqztmUqViRzBk,956
13
13
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
14
  utilities/dataclasses.py,sha256=xbU3QN1GFy7RC6hIJRZIeUZm7YRlodrgEWmahWG6k2g,32465
15
+ utilities/docker.py,sha256=AahXNwtu4ojdVeTdAa-VBR8h7GWNAE3ChHeVBa0oZAE,603
15
16
  utilities/enum.py,sha256=5l6pwZD1cjSlVW4ss-zBPspWvrbrYrdtJWcg6f5_J5w,5781
16
17
  utilities/errors.py,sha256=mFlDGSM0LI1jZ1pbqwLAH3ttLZ2JVIxyZLojw8tGVZU,1479
17
18
  utilities/fastapi.py,sha256=TqyKvBjiMS594sXPjrz-KRTLMb3l3D3rZ1zAYV7GfOk,1454
@@ -28,7 +29,7 @@ utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
28
29
  utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
29
30
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
30
31
  utilities/iterables.py,sha256=t2TsW-K3rVlS6y4_tqcc1fk9RwJV-bi7G_VwduMABK0,42558
31
- utilities/jinja2.py,sha256=jk9GI03mfl0lbu3NFJPRbiZ5xTExGd9lIX-i5SZXiMU,3859
32
+ utilities/jinja2.py,sha256=JpNHMcyMJDguX8rBN4wEz-v4En7w6WHXvYJr4Xw-F0o,4691
32
33
  utilities/json.py,sha256=-WcGtSsCr9Y42wHZzAMnfvU6ihAfVftylFfRUORaDFo,2102
33
34
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
34
35
  utilities/libcst.py,sha256=ngD4wxnR3Kh-RBVmU5l5ST7cuZLhMZwyMDjHZe5mhTs,5581
@@ -49,7 +50,7 @@ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
49
50
  utilities/platform.py,sha256=pTn7gw6N4T6LdKrf0virwarof_mze9WtoQlrGMzhGVI,2798
50
51
  utilities/polars.py,sha256=qsiYY9p_41fORGnc7HNkA4zhlsycK7sgD74xuigMDAc,87466
51
52
  utilities/polars_ols.py,sha256=LNTFNLPuYW7fcAHymlbnams_DhitToblYvib3mhKbwI,5615
52
- utilities/postgres.py,sha256=ynCTTaF-bVEOSW-KEAR-dlLh_hYjeVVjm__-4pEU8Zk,12269
53
+ utilities/postgres.py,sha256=qQzXJngzs2jyZ4rkzL6uBdhXPwiqpPtYQiIonNcGIwI,12516
53
54
  utilities/pottery.py,sha256=nA0SsF9irvfC0tk68YAr08tuL9lGRSlBKihSx7Ibk84,3963
54
55
  utilities/pqdm.py,sha256=idv2seRVP2f6NeSfpeEnT5A-tQezaHZKDyeu16g2-0E,3091
55
56
  utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
@@ -92,8 +93,8 @@ utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
92
93
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
93
94
  utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
94
95
  utilities/pytest_plugins/pytest_regressions.py,sha256=mnHYBfdprz50UGVkVzV1bZERZN5CFfoF8YbokGxdFwU,1639
95
- dycw_utilities-0.166.36.dist-info/METADATA,sha256=Qu3EXmhbVxbw3w5OODwArwNZdxgpLDMEDYf4-VJ24Nw,1699
96
- dycw_utilities-0.166.36.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
- dycw_utilities-0.166.36.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
98
- dycw_utilities-0.166.36.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
99
- dycw_utilities-0.166.36.dist-info/RECORD,,
96
+ dycw_utilities-0.167.0.dist-info/METADATA,sha256=lNFPf9vIPLLTN5FD1LbxTof59uW1K-d8TriQ0JNAlUU,1698
97
+ dycw_utilities-0.167.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
98
+ dycw_utilities-0.167.0.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
99
+ dycw_utilities-0.167.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
100
+ dycw_utilities-0.167.0.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.166.36"
3
+ __version__ = "0.167.0"
utilities/docker.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from collections.abc import Mapping
7
+
8
+
9
+ def docker_exec(
10
+ container: str,
11
+ /,
12
+ *cmd: str,
13
+ env: Mapping[str, str] | None = None,
14
+ **env_kwargs: str | None,
15
+ ) -> list[str]:
16
+ """Run a command through `docker exec`."""
17
+ full = ["docker", "exec"]
18
+ mapping: dict[str, str | None] = ({} if env is None else dict(env)) | env_kwargs
19
+ for key, value in mapping.items():
20
+ full.append(f"--env={key}={value}")
21
+ return [*full, "--interactive", container, *cmd]
22
+
23
+
24
+ __all__ = ["docker_exec"]
utilities/jinja2.py CHANGED
@@ -98,6 +98,12 @@ class TemplateJob:
98
98
  target: Path
99
99
  mode: Literal["write", "append"] = "write"
100
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
+
101
107
  def run(self) -> None:
102
108
  """Run the job."""
103
109
  match self.mode:
@@ -117,4 +123,26 @@ class TemplateJob:
117
123
  return env.get_template(self.template.name).render(self.kwargs)
118
124
 
119
125
 
120
- __all__ = ["EnhancedEnvironment", "TemplateJob"]
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/postgres.py CHANGED
@@ -9,6 +9,7 @@ from sqlalchemy import Table
9
9
  from sqlalchemy.orm import DeclarativeBase
10
10
 
11
11
  from utilities.asyncio import stream_command
12
+ from utilities.docker import docker_exec
12
13
  from utilities.iterables import always_iterable
13
14
  from utilities.logging import to_logger
14
15
  from utilities.os import temp_environ
@@ -37,6 +38,7 @@ async def pg_dump(
37
38
  path: PathLike,
38
39
  /,
39
40
  *,
41
+ docker_container: str | None = None,
40
42
  format_: _PGDumpFormat = "plain",
41
43
  jobs: int | None = None,
42
44
  data_only: bool = False,
@@ -51,7 +53,6 @@ async def pg_dump(
51
53
  inserts: bool = False,
52
54
  on_conflict_do_nothing: bool = False,
53
55
  role: str | None = None,
54
- docker: str | None = None,
55
56
  dry_run: bool = False,
56
57
  logger: LoggerLike | None = None,
57
58
  ) -> bool:
@@ -61,6 +62,7 @@ async def pg_dump(
61
62
  cmd = _build_pg_dump(
62
63
  url,
63
64
  path,
65
+ docker_container=docker_container,
64
66
  format_=format_,
65
67
  jobs=jobs,
66
68
  data_only=data_only,
@@ -75,7 +77,6 @@ async def pg_dump(
75
77
  inserts=inserts,
76
78
  on_conflict_do_nothing=on_conflict_do_nothing,
77
79
  role=role,
78
- docker=docker,
79
80
  )
80
81
  if dry_run:
81
82
  if logger is not None:
@@ -111,6 +112,7 @@ def _build_pg_dump(
111
112
  path: PathLike,
112
113
  /,
113
114
  *,
115
+ docker_container: str | None = None,
114
116
  format_: _PGDumpFormat = "plain",
115
117
  jobs: int | None = None,
116
118
  data_only: bool = False,
@@ -125,12 +127,13 @@ def _build_pg_dump(
125
127
  inserts: bool = False,
126
128
  on_conflict_do_nothing: bool = False,
127
129
  role: str | None = None,
128
- docker: str | None = None,
129
130
  ) -> str:
130
131
  extracted = extract_url(url)
131
132
  path = _path_pg_dump(path, format_=format_)
132
- parts: list[str] = [
133
- "pg_dump",
133
+ parts: list[str] = ["pg_dump"]
134
+ if docker_container is not None:
135
+ parts = docker_exec(docker_container, *parts, PGPASSWORD=extracted.password)
136
+ parts.extend([
134
137
  # general options
135
138
  f"--file={str(path)!r}",
136
139
  f"--format={format_}",
@@ -146,7 +149,7 @@ def _build_pg_dump(
146
149
  f"--port={extracted.port}",
147
150
  f"--username={extracted.username}",
148
151
  "--no-password",
149
- ]
152
+ ])
150
153
  if (format_ == "directory") and (jobs is not None):
151
154
  parts.append(f"--jobs={jobs}")
152
155
  if create:
@@ -173,8 +176,6 @@ def _build_pg_dump(
173
176
  parts.append("--on-conflict-do-nothing")
174
177
  if role is not None:
175
178
  parts.append(f"--role={role}")
176
- if docker is not None:
177
- parts = _wrap_docker(parts, docker)
178
179
  return " ".join(parts)
179
180
 
180
181
 
@@ -213,7 +214,7 @@ async def restore(
213
214
  schema_exc: MaybeCollectionStr | None = None,
214
215
  table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
215
216
  role: str | None = None,
216
- docker: str | None = None,
217
+ docker_container: str | None = None,
217
218
  dry_run: bool = False,
218
219
  logger: LoggerLike | None = None,
219
220
  ) -> bool:
@@ -230,7 +231,7 @@ async def restore(
230
231
  schema_exc=schema_exc,
231
232
  table=table,
232
233
  role=role,
233
- docker=docker,
234
+ docker_container=docker_container,
234
235
  )
235
236
  if dry_run:
236
237
  if logger is not None:
@@ -276,11 +277,11 @@ def _build_pg_restore_or_psql(
276
277
  schema_exc: MaybeCollectionStr | None = None,
277
278
  table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
278
279
  role: str | None = None,
279
- docker: str | None = None,
280
+ docker_container: str | None = None,
280
281
  ) -> str:
281
282
  path = Path(path)
282
283
  if (path.suffix == ".sql") or psql:
283
- return _build_psql(url, path, docker=docker)
284
+ return _build_psql(url, path, docker_container=docker_container)
284
285
  return _build_pg_restore(
285
286
  url,
286
287
  path,
@@ -292,7 +293,7 @@ def _build_pg_restore_or_psql(
292
293
  schemas_exc=schema_exc,
293
294
  tables=table,
294
295
  role=role,
295
- docker=docker,
296
+ docker_container=docker_container,
296
297
  )
297
298
 
298
299
 
@@ -309,12 +310,14 @@ def _build_pg_restore(
309
310
  schemas_exc: MaybeCollectionStr | None = None,
310
311
  tables: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
311
312
  role: str | None = None,
312
- docker: str | None = None,
313
+ docker_container: str | None = None,
313
314
  ) -> str:
314
315
  """Run `pg_restore`."""
315
316
  extracted = extract_url(url)
316
- parts: list[str] = [
317
- "pg_restore",
317
+ parts: list[str] = ["pg_restore"]
318
+ if docker_container is not None:
319
+ parts = docker_exec(docker_container, *parts, PGPASSWORD=extracted.password)
320
+ parts.extend([
318
321
  # general options
319
322
  "--verbose",
320
323
  # restore options
@@ -328,7 +331,7 @@ def _build_pg_restore(
328
331
  f"--username={extracted.username}",
329
332
  f"--dbname={extracted.database}",
330
333
  "--no-password",
331
- ]
334
+ ])
332
335
  if create:
333
336
  parts.append("--create")
334
337
  if jobs is not None:
@@ -341,17 +344,19 @@ def _build_pg_restore(
341
344
  parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(tables)])
342
345
  if role is not None:
343
346
  parts.append(f"--role={role}")
344
- if docker is not None:
345
- parts = _wrap_docker(parts, docker)
346
347
  parts.append(str(path))
347
348
  return " ".join(parts)
348
349
 
349
350
 
350
- def _build_psql(url: URL, path: PathLike, /, *, docker: str | None = None) -> str:
351
+ def _build_psql(
352
+ url: URL, path: PathLike, /, *, docker_container: str | None = None
353
+ ) -> str:
351
354
  """Run `psql`."""
352
355
  extracted = extract_url(url)
353
- parts: list[str] = [
354
- "psql",
356
+ parts: list[str] = ["psql"]
357
+ if docker_container is not None:
358
+ parts = docker_exec(docker_container, *parts, PGPASSWORD=extracted.password)
359
+ parts.extend([
355
360
  # general options
356
361
  f"--dbname={extracted.database}",
357
362
  f"--file={str(path)!r}",
@@ -360,9 +365,7 @@ def _build_psql(url: URL, path: PathLike, /, *, docker: str | None = None) -> st
360
365
  f"--port={extracted.port}",
361
366
  f"--username={extracted.username}",
362
367
  "--no-password",
363
- ]
364
- if docker is not None:
365
- parts = _wrap_docker(parts, docker)
368
+ ])
366
369
  return " ".join(parts)
367
370
 
368
371
 
@@ -402,8 +405,4 @@ class _ResolveDataOnlyAndCleanError(Exception):
402
405
  return "Cannot use '--data-only' and '--clean' together"
403
406
 
404
407
 
405
- def _wrap_docker(parts: list[str], container: str, /) -> list[str]:
406
- return ["docker", "exec", "-it", container, *parts]
407
-
408
-
409
408
  __all__ = ["pg_dump", "restore"]