dycw-utilities 0.148.4__py3-none-any.whl → 0.149.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.
- {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.149.0.dist-info}/METADATA +1 -1
- {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.149.0.dist-info}/RECORD +7 -7
- utilities/__init__.py +1 -1
- utilities/postgres.py +217 -110
- {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.149.0.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.149.0.dist-info}/entry_points.txt +0 -0
- {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.149.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=k25MoUA4CILG827gfaydYpGoUHao6zPu9nXCDQDr9DE,60
|
2
2
|
utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
|
3
3
|
utilities/asyncio.py,sha256=z0w3fb-U5Ml5YXVaFFPClizXaQmjDO6YgZg-V9QL0VQ,16021
|
4
4
|
utilities/atomicwrites.py,sha256=xcOWenTBRS0oat3kg7Sqe51AohNThMQ2ixPL7QCG8hw,5795
|
@@ -48,7 +48,7 @@ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
|
|
48
48
|
utilities/platform.py,sha256=Ue9LSxYvg9yUXGKuz5aZoy_qkUEXde-v6B09exgSctU,2813
|
49
49
|
utilities/polars.py,sha256=BgiDryAVOapi41ddfJqN0wYh_sDj8BNEYtPB36LaHdo,71824
|
50
50
|
utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
|
51
|
-
utilities/postgres.py,sha256=
|
51
|
+
utilities/postgres.py,sha256=ctSaVXmkMwdjpi90tbC8O-08_30Nck99tdykgJQa8Wo,11672
|
52
52
|
utilities/pottery.py,sha256=w2X80PXWwzdHdqSYJP6ESrPNNDP3xzpyuJn-fp-Vt3M,5969
|
53
53
|
utilities/pqdm.py,sha256=BTsYPtbKQWwX-iXF4qCkfPG7DPxIB54J989n83bXrIo,3092
|
54
54
|
utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
|
@@ -88,8 +88,8 @@ utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
|
|
88
88
|
utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
89
89
|
utilities/pytest_plugins/pytest_randomly.py,sha256=NXzCcGKbpgYouz5yehKb4jmxmi2SexKKpgF4M65bi10,414
|
90
90
|
utilities/pytest_plugins/pytest_regressions.py,sha256=Iwhfv_OJH7UCPZCfoh7ugZ2Xjqjil-BBBsOb8sDwiGI,1471
|
91
|
-
dycw_utilities-0.
|
92
|
-
dycw_utilities-0.
|
93
|
-
dycw_utilities-0.
|
94
|
-
dycw_utilities-0.
|
95
|
-
dycw_utilities-0.
|
91
|
+
dycw_utilities-0.149.0.dist-info/METADATA,sha256=AYiiL1k_AquVfj8pssxVFgjD65kqRMCb2DEJMXNAz28,1697
|
92
|
+
dycw_utilities-0.149.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
93
|
+
dycw_utilities-0.149.0.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
|
94
|
+
dycw_utilities-0.149.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
95
|
+
dycw_utilities-0.149.0.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/postgres.py
CHANGED
@@ -12,6 +12,7 @@ from utilities.asyncio import stream_command
|
|
12
12
|
from utilities.iterables import always_iterable
|
13
13
|
from utilities.logging import get_logger
|
14
14
|
from utilities.os import temp_environ
|
15
|
+
from utilities.pathlib import ensure_suffix
|
15
16
|
from utilities.sqlalchemy import get_table_name
|
16
17
|
from utilities.timer import Timer
|
17
18
|
from utilities.types import PathLike
|
@@ -31,7 +32,6 @@ async def pg_dump(
|
|
31
32
|
path: PathLike,
|
32
33
|
/,
|
33
34
|
*,
|
34
|
-
docker: str | None = None,
|
35
35
|
format_: _PGDumpFormat = "plain",
|
36
36
|
jobs: int | None = None,
|
37
37
|
schemas: MaybeListStr | None = None,
|
@@ -39,26 +39,93 @@ async def pg_dump(
|
|
39
39
|
tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
40
40
|
tables_exc: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
41
41
|
inserts: bool = False,
|
42
|
-
|
42
|
+
on_conflict_do_nothing: bool = False,
|
43
|
+
docker: str | None = None,
|
43
44
|
dry_run: bool = False,
|
45
|
+
logger: LoggerOrName | None = None,
|
44
46
|
) -> None:
|
45
47
|
"""Run `pg_dump`."""
|
46
48
|
path = Path(path)
|
47
49
|
path.parent.mkdir(parents=True, exist_ok=True)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
50
|
+
cmd = _build_pg_dump(
|
51
|
+
url,
|
52
|
+
path,
|
53
|
+
format_=format_,
|
54
|
+
jobs=jobs,
|
55
|
+
schemas=schemas,
|
56
|
+
schemas_exc=schemas_exc,
|
57
|
+
tables=tables,
|
58
|
+
tables_exc=tables_exc,
|
59
|
+
inserts=inserts,
|
60
|
+
on_conflict_do_nothing=on_conflict_do_nothing,
|
61
|
+
docker=docker,
|
62
|
+
)
|
63
|
+
if dry_run:
|
64
|
+
if logger is not None:
|
65
|
+
get_logger(logger=logger).info("Would run %r", str(cmd))
|
66
|
+
return
|
67
|
+
with temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
|
68
|
+
try:
|
69
|
+
output = await stream_command(cmd)
|
70
|
+
except KeyboardInterrupt:
|
71
|
+
if logger is not None:
|
72
|
+
get_logger(logger=logger).info(
|
73
|
+
"Cancelled backup to %r after %s", str(path), timer
|
74
|
+
)
|
75
|
+
rmtree(path, ignore_errors=True)
|
76
|
+
else:
|
77
|
+
match output.return_code:
|
78
|
+
case 0:
|
79
|
+
if logger is not None:
|
80
|
+
get_logger(logger=logger).info(
|
81
|
+
"Backup to %r finished after %s", str(path), timer
|
82
|
+
)
|
83
|
+
case _:
|
84
|
+
if logger is not None:
|
85
|
+
get_logger(logger=logger).exception(
|
86
|
+
"Backup to %r failed after %s\nstderr:\n%s",
|
87
|
+
str(path),
|
88
|
+
timer,
|
89
|
+
output.stderr,
|
90
|
+
)
|
91
|
+
rmtree(path, ignore_errors=True)
|
92
|
+
|
93
|
+
|
94
|
+
def _build_pg_dump(
|
95
|
+
url: URL,
|
96
|
+
path: PathLike,
|
97
|
+
/,
|
98
|
+
*,
|
99
|
+
format_: _PGDumpFormat = "plain",
|
100
|
+
jobs: int | None = None,
|
101
|
+
schemas: MaybeListStr | None = None,
|
102
|
+
schemas_exc: MaybeListStr | None = None,
|
103
|
+
tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
104
|
+
tables_exc: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
105
|
+
inserts: bool = False,
|
106
|
+
on_conflict_do_nothing: bool = False,
|
107
|
+
docker: str | None = None,
|
108
|
+
) -> str:
|
109
|
+
database, host, port = _extract_url(url)
|
110
|
+
match format_:
|
111
|
+
case "plain":
|
112
|
+
suffix = ".sql"
|
113
|
+
case "custom":
|
114
|
+
suffix = ".pgdump"
|
115
|
+
case "directory":
|
116
|
+
suffix = None
|
117
|
+
case "tar":
|
118
|
+
suffix = ".tar"
|
119
|
+
case _ as never:
|
120
|
+
assert_never(never)
|
121
|
+
file = Path(path)
|
122
|
+
if suffix is not None:
|
123
|
+
file = ensure_suffix(file, suffix)
|
124
|
+
parts: list[str] = [
|
58
125
|
"pg_dump",
|
59
126
|
# general options
|
60
|
-
f"--dbname={
|
61
|
-
f"--file={str(
|
127
|
+
f"--dbname={database}",
|
128
|
+
f"--file={str(file)!r}",
|
62
129
|
f"--format={format_}",
|
63
130
|
"--verbose",
|
64
131
|
# output options
|
@@ -68,10 +135,10 @@ async def pg_dump(
|
|
68
135
|
"--no-privileges",
|
69
136
|
"--if-exists",
|
70
137
|
# connection options
|
71
|
-
f"--host={
|
72
|
-
f"--port={
|
138
|
+
f"--host={host}",
|
139
|
+
f"--port={port}",
|
73
140
|
"--no-password",
|
74
|
-
]
|
141
|
+
]
|
75
142
|
if (format_ == "directory") and (jobs is not None):
|
76
143
|
parts.append(f"--jobs={jobs}")
|
77
144
|
if schemas is not None:
|
@@ -86,9 +153,47 @@ async def pg_dump(
|
|
86
153
|
])
|
87
154
|
if inserts:
|
88
155
|
parts.append("--inserts")
|
156
|
+
if on_conflict_do_nothing:
|
157
|
+
parts.append("--on-conflict-do-nothing")
|
89
158
|
if url.username is not None:
|
90
159
|
parts.append(f"--username={url.username}")
|
91
|
-
|
160
|
+
if docker is not None:
|
161
|
+
parts = _wrap_docker(parts, docker)
|
162
|
+
return " ".join(parts)
|
163
|
+
|
164
|
+
|
165
|
+
##
|
166
|
+
|
167
|
+
|
168
|
+
async def restore(
|
169
|
+
url: URL,
|
170
|
+
path: PathLike,
|
171
|
+
/,
|
172
|
+
*,
|
173
|
+
psql: bool = False,
|
174
|
+
database: str | None = None,
|
175
|
+
data_only: bool = False,
|
176
|
+
jobs: int | None = None,
|
177
|
+
schemas: MaybeListStr | None = None,
|
178
|
+
schemas_exc: MaybeListStr | None = None,
|
179
|
+
tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
180
|
+
docker: str | None = None,
|
181
|
+
dry_run: bool = False,
|
182
|
+
logger: LoggerOrName | None = None,
|
183
|
+
) -> None:
|
184
|
+
"""Run `pg_restore`/`psql`."""
|
185
|
+
cmd = _build_pg_restore_or_psql(
|
186
|
+
url,
|
187
|
+
path,
|
188
|
+
psql=psql,
|
189
|
+
database=database,
|
190
|
+
data_only=data_only,
|
191
|
+
jobs=jobs,
|
192
|
+
schemas=schemas,
|
193
|
+
schemas_exc=schemas_exc,
|
194
|
+
tables=tables,
|
195
|
+
docker=docker,
|
196
|
+
)
|
92
197
|
if dry_run:
|
93
198
|
if logger is not None:
|
94
199
|
get_logger(logger=logger).info("Would run %r", str(cmd))
|
@@ -99,89 +204,75 @@ async def pg_dump(
|
|
99
204
|
except KeyboardInterrupt:
|
100
205
|
if logger is not None:
|
101
206
|
get_logger(logger=logger).info(
|
102
|
-
"Cancelled
|
207
|
+
"Cancelled restore from %r after %s", str(path), timer
|
103
208
|
)
|
104
|
-
rmtree(path, ignore_errors=True)
|
105
209
|
else:
|
106
210
|
match output.return_code:
|
107
211
|
case 0:
|
108
212
|
if logger is not None:
|
109
213
|
get_logger(logger=logger).info(
|
110
|
-
"
|
214
|
+
"Restore from %r finished after %s", str(path), timer
|
111
215
|
)
|
112
216
|
case _:
|
113
217
|
if logger is not None:
|
114
218
|
get_logger(logger=logger).exception(
|
115
|
-
"
|
219
|
+
"Restore from %r failed after %s\nstderr:\n%s",
|
116
220
|
str(path),
|
117
221
|
timer,
|
118
222
|
output.stderr,
|
119
223
|
)
|
120
|
-
rmtree(path, ignore_errors=True)
|
121
|
-
|
122
|
-
|
123
|
-
@dataclass(kw_only=True, slots=True)
|
124
|
-
class PGDumpError(Exception):
|
125
|
-
url: URL
|
126
|
-
|
127
|
-
|
128
|
-
@dataclass(kw_only=True, slots=True)
|
129
|
-
class _PGDumpDatabaseError(PGDumpError):
|
130
|
-
@override
|
131
|
-
def __str__(self) -> str:
|
132
|
-
return f"Expected URL to contain a 'database'; got {self.url}"
|
133
|
-
|
134
|
-
|
135
|
-
@dataclass(kw_only=True, slots=True)
|
136
|
-
class _PGDumpHostError(PGDumpError):
|
137
|
-
@override
|
138
|
-
def __str__(self) -> str:
|
139
|
-
return f"Expected URL to contain a 'host'; got {self.url}"
|
140
|
-
|
141
|
-
|
142
|
-
@dataclass(kw_only=True, slots=True)
|
143
|
-
class _PGDumpPortError(PGDumpError):
|
144
|
-
@override
|
145
|
-
def __str__(self) -> str:
|
146
|
-
return f"Expected URL to contain a 'port'; got {self.url}"
|
147
224
|
|
148
225
|
|
149
226
|
##
|
150
227
|
|
151
228
|
|
152
|
-
|
229
|
+
def _build_pg_restore_or_psql(
|
153
230
|
url: URL,
|
154
231
|
path: PathLike,
|
155
232
|
/,
|
156
233
|
*,
|
234
|
+
psql: bool = False,
|
157
235
|
database: str | None = None,
|
236
|
+
data_only: bool = False,
|
237
|
+
jobs: int | None = None,
|
238
|
+
schemas: MaybeListStr | None = None,
|
239
|
+
schemas_exc: MaybeListStr | None = None,
|
240
|
+
tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
158
241
|
docker: str | None = None,
|
242
|
+
) -> str:
|
243
|
+
path = Path(path)
|
244
|
+
if (path.suffix == ".sql") or psql:
|
245
|
+
return _build_psql(url, path, database=database, docker=docker)
|
246
|
+
return _build_pg_restore(
|
247
|
+
url,
|
248
|
+
path,
|
249
|
+
database=database,
|
250
|
+
data_only=data_only,
|
251
|
+
jobs=jobs,
|
252
|
+
schemas=schemas,
|
253
|
+
schemas_exc=schemas_exc,
|
254
|
+
tables=tables,
|
255
|
+
docker=docker,
|
256
|
+
)
|
257
|
+
|
258
|
+
|
259
|
+
def _build_pg_restore(
|
260
|
+
url: URL,
|
261
|
+
path: PathLike,
|
262
|
+
/,
|
263
|
+
*,
|
264
|
+
database: str | None = None,
|
159
265
|
data_only: bool = False,
|
160
266
|
jobs: int | None = None,
|
161
267
|
schemas: MaybeListStr | None = None,
|
162
268
|
schemas_exc: MaybeListStr | None = None,
|
163
269
|
tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
|
164
|
-
|
165
|
-
|
166
|
-
) -> None:
|
270
|
+
docker: str | None = None,
|
271
|
+
) -> str:
|
167
272
|
"""Run `pg_restore`."""
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
case None, str() as database_use:
|
172
|
-
...
|
173
|
-
case None, None:
|
174
|
-
raise _PGRestoreDatabaseError(url=url)
|
175
|
-
case _ as never:
|
176
|
-
assert_never(never)
|
177
|
-
if url.host is None:
|
178
|
-
raise _PGRestoreHostError(url=url)
|
179
|
-
if url.port is None:
|
180
|
-
raise _PGRestorePortError(url=url)
|
181
|
-
parts: list[str] = []
|
182
|
-
if docker is not None:
|
183
|
-
parts.extend(["docker", "exec", "-it", docker])
|
184
|
-
parts.extend([
|
273
|
+
url_database, host, port = _extract_url(url)
|
274
|
+
database_use = url_database if database is None else database
|
275
|
+
parts: list[str] = [
|
185
276
|
"pg_restore",
|
186
277
|
# general options
|
187
278
|
f"--dbname={database_use}",
|
@@ -191,10 +282,10 @@ async def pg_restore(
|
|
191
282
|
"--no-owner",
|
192
283
|
"--no-privileges",
|
193
284
|
# connection options
|
194
|
-
f"--host={
|
195
|
-
f"--port={
|
285
|
+
f"--host={host}",
|
286
|
+
f"--port={port}",
|
196
287
|
"--no-password",
|
197
|
-
]
|
288
|
+
]
|
198
289
|
if data_only:
|
199
290
|
parts.append("--data-only")
|
200
291
|
else:
|
@@ -209,68 +300,80 @@ async def pg_restore(
|
|
209
300
|
parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(tables)])
|
210
301
|
if url.username is not None:
|
211
302
|
parts.append(f"--username={url.username}")
|
303
|
+
if docker is not None:
|
304
|
+
parts = _wrap_docker(parts, docker)
|
212
305
|
parts.append(str(path))
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
306
|
+
return " ".join(parts)
|
307
|
+
|
308
|
+
|
309
|
+
def _build_psql(
|
310
|
+
url: URL,
|
311
|
+
path: PathLike,
|
312
|
+
/,
|
313
|
+
*,
|
314
|
+
database: str | None = None,
|
315
|
+
docker: str | None = None,
|
316
|
+
) -> str:
|
317
|
+
"""Run `psql`."""
|
318
|
+
url_database, host, port = _extract_url(url)
|
319
|
+
database_use = url_database if database is None else database
|
320
|
+
parts: list[str] = [
|
321
|
+
"psql",
|
322
|
+
# general options
|
323
|
+
f"--dbname={database_use}",
|
324
|
+
f"--file={str(path)!r}",
|
325
|
+
# connection options
|
326
|
+
f"--host={host}",
|
327
|
+
f"--port={port}",
|
328
|
+
"--no-password",
|
329
|
+
]
|
330
|
+
if url.username is not None:
|
331
|
+
parts.append(f"--username={url.username}")
|
332
|
+
if docker is not None:
|
333
|
+
parts = _wrap_docker(parts, docker)
|
334
|
+
return " ".join(parts)
|
335
|
+
|
336
|
+
|
337
|
+
##
|
338
|
+
|
339
|
+
|
340
|
+
def _extract_url(url: URL, /) -> tuple[str, str, int]:
|
341
|
+
if url.database is None:
|
342
|
+
raise _ExtractURLDatabaseError(url=url)
|
343
|
+
if url.host is None:
|
344
|
+
raise _ExtractURLHostError(url=url)
|
345
|
+
if url.port is None:
|
346
|
+
raise _ExtractURLPortError(url=url)
|
347
|
+
return url.database, url.host, url.port
|
241
348
|
|
242
349
|
|
243
350
|
@dataclass(kw_only=True, slots=True)
|
244
|
-
class
|
351
|
+
class ExtractURLError(Exception):
|
245
352
|
url: URL
|
246
353
|
|
247
354
|
|
248
355
|
@dataclass(kw_only=True, slots=True)
|
249
|
-
class
|
356
|
+
class _ExtractURLDatabaseError(ExtractURLError):
|
250
357
|
@override
|
251
358
|
def __str__(self) -> str:
|
252
359
|
return f"Expected URL to contain a 'database'; got {self.url}"
|
253
360
|
|
254
361
|
|
255
362
|
@dataclass(kw_only=True, slots=True)
|
256
|
-
class
|
363
|
+
class _ExtractURLHostError(ExtractURLError):
|
257
364
|
@override
|
258
365
|
def __str__(self) -> str:
|
259
366
|
return f"Expected URL to contain a 'host'; got {self.url}"
|
260
367
|
|
261
368
|
|
262
369
|
@dataclass(kw_only=True, slots=True)
|
263
|
-
class
|
370
|
+
class _ExtractURLPortError(ExtractURLError):
|
264
371
|
@override
|
265
372
|
def __str__(self) -> str:
|
266
373
|
return f"Expected URL to contain a 'port'; got {self.url}"
|
267
374
|
|
268
375
|
|
269
|
-
##
|
270
|
-
|
271
|
-
|
272
376
|
def _get_table_name(obj: TableOrORMInstOrClass | str, /) -> str:
|
273
|
-
"""Get the table name from a Table or mapped class."""
|
274
377
|
match obj:
|
275
378
|
case Table() | DeclarativeBase() | type() as table_or_orm:
|
276
379
|
return get_table_name(table_or_orm)
|
@@ -280,4 +383,8 @@ def _get_table_name(obj: TableOrORMInstOrClass | str, /) -> str:
|
|
280
383
|
assert_never(never)
|
281
384
|
|
282
385
|
|
283
|
-
|
386
|
+
def _wrap_docker(parts: list[str], container: str, /) -> list[str]:
|
387
|
+
return ["docker", "exec", "-it", container, *parts]
|
388
|
+
|
389
|
+
|
390
|
+
__all__ = ["ExtractURLError", "pg_dump", "restore"]
|
File without changes
|
File without changes
|
File without changes
|