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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.166.37
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.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=5A2yZ5q_GCT119LdZKfbPBpyhrtF0D3ytDUhvCLLd1s,61
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=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
@@ -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=X_F4Vq3t0ftgmhYdrs1DOMrI4ls5Tw_ddcW7dmhQaPY,36407
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.166.37.dist-info/METADATA,sha256=rz6vDyAduIOHCHNE2PwjwCjAi6Oj886M54iwgmo7Bxk,1699
96
- dycw_utilities-0.166.37.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
- dycw_utilities-0.166.37.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
98
- dycw_utilities-0.166.37.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
99
- dycw_utilities-0.166.37.dist-info/RECORD,,
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
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.166.37"
3
+ __version__ = "0.167.1"
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
- "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"]
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",