dycw-utilities 0.166.37__py3-none-any.whl → 0.167.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.
- {dycw_utilities-0.166.37.dist-info → dycw_utilities-0.167.1.dist-info}/METADATA +2 -2
- {dycw_utilities-0.166.37.dist-info → dycw_utilities-0.167.1.dist-info}/RECORD +9 -8
- utilities/__init__.py +1 -1
- utilities/docker.py +24 -0
- utilities/postgres.py +28 -29
- utilities/sqlalchemy.py +15 -0
- {dycw_utilities-0.166.37.dist-info → dycw_utilities-0.167.1.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.166.37.dist-info → dycw_utilities-0.167.1.dist-info}/entry_points.txt +0 -0
- {dycw_utilities-0.166.37.dist-info → dycw_utilities-0.167.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dycw-utilities
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.167.1
|
|
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.
|
|
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=
|
|
1
|
+
utilities/__init__.py,sha256=5n59LyIEvqGZR7Atiaj1WopkM0tKCDhQyUb2l-MH_qc,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
|
|
@@ -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=
|
|
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
|
|
@@ -68,7 +69,7 @@ utilities/sentinel.py,sha256=A_p5jX2K0Yc5XBfoYHyBLqHsEWzE1ByOdDuzzA2pZnE,1434
|
|
|
68
69
|
utilities/shelve.py,sha256=4OzjQI6kGuUbJciqf535rwnao-_IBv66gsT6tRGiUt0,759
|
|
69
70
|
utilities/slack_sdk.py,sha256=76-DYtcGiUhEvl-voMamc5OjfF7Y7nCq54Bys1arqzw,2233
|
|
70
71
|
utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
|
|
71
|
-
utilities/sqlalchemy.py,sha256=
|
|
72
|
+
utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
|
|
72
73
|
utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
|
|
73
74
|
utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
|
|
74
75
|
utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
|
|
@@ -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.
|
|
96
|
-
dycw_utilities-0.
|
|
97
|
-
dycw_utilities-0.
|
|
98
|
-
dycw_utilities-0.
|
|
99
|
-
dycw_utilities-0.
|
|
96
|
+
dycw_utilities-0.167.1.dist-info/METADATA,sha256=YDvpCOu6_rWr8_UnnjOzy0UM4JQABa4nltfz7kkNkLU,1698
|
|
97
|
+
dycw_utilities-0.167.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
98
|
+
dycw_utilities-0.167.1.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
|
|
99
|
+
dycw_utilities-0.167.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
|
100
|
+
dycw_utilities-0.167.1.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
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/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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"]
|
utilities/sqlalchemy.py
CHANGED
|
@@ -331,6 +331,20 @@ async def ensure_database_dropped(super_: URL, database: str, /) -> None:
|
|
|
331
331
|
_ = await conn.execute(text(f"DROP DATABASE IF EXISTS {database}"))
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
async def ensure_database_users_disconnected(super_: URL, database: str, /) -> None:
|
|
335
|
+
"""Ensure a databases' users are disconnected."""
|
|
336
|
+
engine = create_async_engine(super_, isolation_level="AUTOCOMMIT")
|
|
337
|
+
match dialect := _get_dialect(engine):
|
|
338
|
+
case "postgresql": # skipif-ci-and-not-linux
|
|
339
|
+
query = f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = {database!r} AND pid <> pg_backend_pid()" # noqa: S608
|
|
340
|
+
case "mssql" | "mysql" | "oracle" | "sqlite": # pragma: no cover
|
|
341
|
+
raise NotImplementedError(dialect)
|
|
342
|
+
case never:
|
|
343
|
+
assert_never(never)
|
|
344
|
+
async with engine.begin() as conn:
|
|
345
|
+
_ = await conn.execute(text(query))
|
|
346
|
+
|
|
347
|
+
|
|
334
348
|
##
|
|
335
349
|
|
|
336
350
|
|
|
@@ -1166,6 +1180,7 @@ __all__ = [
|
|
|
1166
1180
|
"create_engine",
|
|
1167
1181
|
"ensure_database_created",
|
|
1168
1182
|
"ensure_database_dropped",
|
|
1183
|
+
"ensure_database_users_disconnected",
|
|
1169
1184
|
"ensure_tables_created",
|
|
1170
1185
|
"ensure_tables_dropped",
|
|
1171
1186
|
"enum_name",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|