dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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.175.31.dist-info/METADATA +34 -0
- dycw_utilities-0.175.31.dist-info/RECORD +103 -0
- dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
- {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +113 -64
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +381 -0
- utilities/enum.py +2 -2
- utilities/errors.py +1 -1
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +12 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +74 -85
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +4 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +296 -174
- utilities/pottery.py +8 -73
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +155 -46
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +8 -3
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1947 -0
- utilities/tempfile.py +95 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +2 -2
- utilities/traceback.py +46 -36
- utilities/types.py +62 -23
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +661 -151
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.148.5.dist-info/METADATA +0 -41
- dycw_utilities-0.148.5.dist-info/RECORD +0 -95
- dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
- dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/typed_settings.py +0 -144
utilities/polars_ols.py
CHANGED
|
@@ -6,8 +6,8 @@ from polars import Expr, Series, struct
|
|
|
6
6
|
from polars_ols import RollingKwargs, compute_rolling_least_squares
|
|
7
7
|
|
|
8
8
|
from utilities.errors import ImpossibleCaseError
|
|
9
|
-
from utilities.functions import is_sequence_of
|
|
10
9
|
from utilities.polars import concat_series, ensure_expr_or_series
|
|
10
|
+
from utilities.typing import is_sequence_of
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from polars._typing import IntoExprColumn
|
utilities/postgres.py
CHANGED
|
@@ -9,10 +9,12 @@ 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_cmd
|
|
12
13
|
from utilities.iterables import always_iterable
|
|
13
|
-
from utilities.logging import
|
|
14
|
+
from utilities.logging import to_logger
|
|
14
15
|
from utilities.os import temp_environ
|
|
15
|
-
from utilities.
|
|
16
|
+
from utilities.pathlib import ensure_suffix
|
|
17
|
+
from utilities.sqlalchemy import extract_url, get_table_name
|
|
16
18
|
from utilities.timer import Timer
|
|
17
19
|
from utilities.types import PathLike
|
|
18
20
|
|
|
@@ -20,7 +22,12 @@ if TYPE_CHECKING:
|
|
|
20
22
|
from sqlalchemy import URL
|
|
21
23
|
|
|
22
24
|
from utilities.sqlalchemy import TableOrORMInstOrClass
|
|
23
|
-
from utilities.types import
|
|
25
|
+
from utilities.types import (
|
|
26
|
+
LoggerLike,
|
|
27
|
+
MaybeCollection,
|
|
28
|
+
MaybeCollectionStr,
|
|
29
|
+
PathLike,
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
|
|
26
33
|
type _PGDumpFormat = Literal["plain", "custom", "directory", "tar"]
|
|
@@ -31,177 +38,302 @@ async def pg_dump(
|
|
|
31
38
|
path: PathLike,
|
|
32
39
|
/,
|
|
33
40
|
*,
|
|
34
|
-
|
|
41
|
+
docker_container: str | None = None,
|
|
35
42
|
format_: _PGDumpFormat = "plain",
|
|
36
43
|
jobs: int | None = None,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
data_only: bool = False,
|
|
45
|
+
clean: bool = False,
|
|
46
|
+
create: bool = False,
|
|
47
|
+
extension: MaybeCollectionStr | None = None,
|
|
48
|
+
extension_exc: MaybeCollectionStr | None = None,
|
|
49
|
+
schema: MaybeCollectionStr | None = None,
|
|
50
|
+
schema_exc: MaybeCollectionStr | None = None,
|
|
51
|
+
table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
52
|
+
table_exc: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
41
53
|
inserts: bool = False,
|
|
42
54
|
on_conflict_do_nothing: bool = False,
|
|
43
|
-
|
|
55
|
+
role: str | None = None,
|
|
44
56
|
dry_run: bool = False,
|
|
45
|
-
|
|
57
|
+
logger: LoggerLike | None = None,
|
|
58
|
+
) -> bool:
|
|
46
59
|
"""Run `pg_dump`."""
|
|
47
|
-
path =
|
|
60
|
+
path = _path_pg_dump(path, format_=format_)
|
|
48
61
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
cmd = _build_pg_dump(
|
|
63
|
+
url,
|
|
64
|
+
path,
|
|
65
|
+
docker_container=docker_container,
|
|
66
|
+
format_=format_,
|
|
67
|
+
jobs=jobs,
|
|
68
|
+
data_only=data_only,
|
|
69
|
+
clean=clean,
|
|
70
|
+
create=create,
|
|
71
|
+
extension=extension,
|
|
72
|
+
extension_exc=extension_exc,
|
|
73
|
+
schema=schema,
|
|
74
|
+
schema_exc=schema_exc,
|
|
75
|
+
table=table,
|
|
76
|
+
table_exc=table_exc,
|
|
77
|
+
inserts=inserts,
|
|
78
|
+
on_conflict_do_nothing=on_conflict_do_nothing,
|
|
79
|
+
role=role,
|
|
80
|
+
)
|
|
81
|
+
if dry_run:
|
|
82
|
+
if logger is not None:
|
|
83
|
+
to_logger(logger).info("Would run:\n\t%r", str(cmd))
|
|
84
|
+
return True
|
|
85
|
+
with temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
|
|
86
|
+
try:
|
|
87
|
+
output = await stream_command(cmd)
|
|
88
|
+
except KeyboardInterrupt:
|
|
89
|
+
if logger is not None:
|
|
90
|
+
to_logger(logger).info(
|
|
91
|
+
"Cancelled backup to %r after %s", str(path), timer
|
|
92
|
+
)
|
|
93
|
+
rmtree(path, ignore_errors=True)
|
|
94
|
+
return False
|
|
95
|
+
if output.return_code != 0:
|
|
96
|
+
if logger is not None:
|
|
97
|
+
to_logger(logger).exception(
|
|
98
|
+
"Backup to %r failed after %s\nstderr:\n%s",
|
|
99
|
+
str(path),
|
|
100
|
+
timer,
|
|
101
|
+
output.stderr,
|
|
102
|
+
)
|
|
103
|
+
rmtree(path, ignore_errors=True)
|
|
104
|
+
return False
|
|
105
|
+
if logger is not None: # pragma: no cover
|
|
106
|
+
to_logger(logger).info("Backup to %r finished after %s", str(path), timer)
|
|
107
|
+
return True # pragma: no cover
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _build_pg_dump(
|
|
111
|
+
url: URL,
|
|
112
|
+
path: PathLike,
|
|
113
|
+
/,
|
|
114
|
+
*,
|
|
115
|
+
docker_container: str | None = None,
|
|
116
|
+
format_: _PGDumpFormat = "plain",
|
|
117
|
+
jobs: int | None = None,
|
|
118
|
+
data_only: bool = False,
|
|
119
|
+
clean: bool = False,
|
|
120
|
+
create: bool = False,
|
|
121
|
+
extension: MaybeCollectionStr | None = None,
|
|
122
|
+
extension_exc: MaybeCollectionStr | None = None,
|
|
123
|
+
schema: MaybeCollectionStr | None = None,
|
|
124
|
+
schema_exc: MaybeCollectionStr | None = None,
|
|
125
|
+
table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
126
|
+
table_exc: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
127
|
+
inserts: bool = False,
|
|
128
|
+
on_conflict_do_nothing: bool = False,
|
|
129
|
+
role: str | None = None,
|
|
130
|
+
) -> str:
|
|
131
|
+
extracted = extract_url(url)
|
|
132
|
+
path = _path_pg_dump(path, format_=format_)
|
|
133
|
+
parts: list[str] = ["pg_dump"]
|
|
134
|
+
if docker_container is not None:
|
|
135
|
+
parts = docker_exec_cmd(docker_container, *parts, PGPASSWORD=extracted.password)
|
|
58
136
|
parts.extend([
|
|
59
|
-
"pg_dump",
|
|
60
137
|
# general options
|
|
61
|
-
f"--dbname={url.database}",
|
|
62
138
|
f"--file={str(path)!r}",
|
|
63
139
|
f"--format={format_}",
|
|
64
140
|
"--verbose",
|
|
65
141
|
# output options
|
|
142
|
+
*_resolve_data_only_and_clean(data_only=data_only, clean=clean),
|
|
66
143
|
"--large-objects",
|
|
67
|
-
"--clean",
|
|
68
144
|
"--no-owner",
|
|
69
145
|
"--no-privileges",
|
|
70
|
-
"--if-exists",
|
|
71
146
|
# connection options
|
|
72
|
-
f"--
|
|
73
|
-
f"--
|
|
147
|
+
f"--dbname={extracted.database}",
|
|
148
|
+
f"--host={extracted.host}",
|
|
149
|
+
f"--port={extracted.port}",
|
|
150
|
+
f"--username={extracted.username}",
|
|
74
151
|
"--no-password",
|
|
75
152
|
])
|
|
76
153
|
if (format_ == "directory") and (jobs is not None):
|
|
77
154
|
parts.append(f"--jobs={jobs}")
|
|
78
|
-
if
|
|
79
|
-
parts.
|
|
80
|
-
if
|
|
81
|
-
parts.extend([f"--
|
|
82
|
-
if
|
|
83
|
-
parts.extend([
|
|
84
|
-
|
|
155
|
+
if create:
|
|
156
|
+
parts.append("--create")
|
|
157
|
+
if extension is not None:
|
|
158
|
+
parts.extend([f"--extension={e}" for e in always_iterable(extension)])
|
|
159
|
+
if extension_exc is not None:
|
|
160
|
+
parts.extend([
|
|
161
|
+
f"--exclude-extension={e}" for e in always_iterable(extension_exc)
|
|
162
|
+
])
|
|
163
|
+
if schema is not None:
|
|
164
|
+
parts.extend([f"--schema={s}" for s in always_iterable(schema)])
|
|
165
|
+
if schema_exc is not None:
|
|
166
|
+
parts.extend([f"--exclude-schema={s}" for s in always_iterable(schema_exc)])
|
|
167
|
+
if table is not None:
|
|
168
|
+
parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(table)])
|
|
169
|
+
if table_exc is not None:
|
|
85
170
|
parts.extend([
|
|
86
|
-
f"--exclude-table={_get_table_name(t)}" for t in always_iterable(
|
|
171
|
+
f"--exclude-table={_get_table_name(t)}" for t in always_iterable(table_exc)
|
|
87
172
|
])
|
|
88
173
|
if inserts:
|
|
89
174
|
parts.append("--inserts")
|
|
90
175
|
if on_conflict_do_nothing:
|
|
91
176
|
parts.append("--on-conflict-do-nothing")
|
|
92
|
-
if
|
|
93
|
-
parts.append(f"--
|
|
94
|
-
|
|
177
|
+
if role is not None:
|
|
178
|
+
parts.append(f"--role={role}")
|
|
179
|
+
return " ".join(parts)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _path_pg_dump(path: PathLike, /, *, format_: _PGDumpFormat = "plain") -> Path:
|
|
183
|
+
match format_:
|
|
184
|
+
case "plain":
|
|
185
|
+
suffix = ".sql"
|
|
186
|
+
case "custom":
|
|
187
|
+
suffix = ".pgdump"
|
|
188
|
+
case "directory":
|
|
189
|
+
suffix = None
|
|
190
|
+
case "tar":
|
|
191
|
+
suffix = ".tar"
|
|
192
|
+
case never:
|
|
193
|
+
assert_never(never)
|
|
194
|
+
path = Path(path)
|
|
195
|
+
if suffix is not None:
|
|
196
|
+
path = ensure_suffix(path, suffix)
|
|
197
|
+
return path
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
##
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def restore(
|
|
204
|
+
url: URL,
|
|
205
|
+
path: PathLike,
|
|
206
|
+
/,
|
|
207
|
+
*,
|
|
208
|
+
psql: bool = False,
|
|
209
|
+
data_only: bool = False,
|
|
210
|
+
clean: bool = False,
|
|
211
|
+
create: bool = False,
|
|
212
|
+
jobs: int | None = None,
|
|
213
|
+
schema: MaybeCollectionStr | None = None,
|
|
214
|
+
schema_exc: MaybeCollectionStr | None = None,
|
|
215
|
+
table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
216
|
+
role: str | None = None,
|
|
217
|
+
docker_container: str | None = None,
|
|
218
|
+
dry_run: bool = False,
|
|
219
|
+
logger: LoggerLike | None = None,
|
|
220
|
+
) -> bool:
|
|
221
|
+
"""Run `pg_restore`/`psql`."""
|
|
222
|
+
cmd = _build_pg_restore_or_psql(
|
|
223
|
+
url,
|
|
224
|
+
path,
|
|
225
|
+
psql=psql,
|
|
226
|
+
data_only=data_only,
|
|
227
|
+
clean=clean,
|
|
228
|
+
create=create,
|
|
229
|
+
jobs=jobs,
|
|
230
|
+
schema=schema,
|
|
231
|
+
schema_exc=schema_exc,
|
|
232
|
+
table=table,
|
|
233
|
+
role=role,
|
|
234
|
+
docker_container=docker_container,
|
|
235
|
+
)
|
|
95
236
|
if dry_run:
|
|
96
237
|
if logger is not None:
|
|
97
|
-
|
|
98
|
-
return
|
|
238
|
+
to_logger(logger).info("Would run:\n\t%r", str(cmd))
|
|
239
|
+
return True
|
|
99
240
|
with temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
|
|
100
241
|
try:
|
|
101
242
|
output = await stream_command(cmd)
|
|
102
243
|
except KeyboardInterrupt:
|
|
103
244
|
if logger is not None:
|
|
104
|
-
|
|
105
|
-
"Cancelled
|
|
245
|
+
to_logger(logger).info(
|
|
246
|
+
"Cancelled restore from %r after %s", str(path), timer
|
|
106
247
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
timer,
|
|
121
|
-
output.stderr,
|
|
122
|
-
)
|
|
123
|
-
rmtree(path, ignore_errors=True)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@dataclass(kw_only=True, slots=True)
|
|
127
|
-
class PGDumpError(Exception):
|
|
128
|
-
url: URL
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@dataclass(kw_only=True, slots=True)
|
|
132
|
-
class _PGDumpDatabaseError(PGDumpError):
|
|
133
|
-
@override
|
|
134
|
-
def __str__(self) -> str:
|
|
135
|
-
return f"Expected URL to contain a 'database'; got {self.url}"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@dataclass(kw_only=True, slots=True)
|
|
139
|
-
class _PGDumpHostError(PGDumpError):
|
|
140
|
-
@override
|
|
141
|
-
def __str__(self) -> str:
|
|
142
|
-
return f"Expected URL to contain a 'host'; got {self.url}"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@dataclass(kw_only=True, slots=True)
|
|
146
|
-
class _PGDumpPortError(PGDumpError):
|
|
147
|
-
@override
|
|
148
|
-
def __str__(self) -> str:
|
|
149
|
-
return f"Expected URL to contain a 'port'; got {self.url}"
|
|
248
|
+
return False
|
|
249
|
+
if output.return_code != 0:
|
|
250
|
+
if logger is not None:
|
|
251
|
+
to_logger(logger).exception(
|
|
252
|
+
"Restore from %r failed after %s\nstderr:\n%s",
|
|
253
|
+
str(path),
|
|
254
|
+
timer,
|
|
255
|
+
output.stderr,
|
|
256
|
+
)
|
|
257
|
+
return False
|
|
258
|
+
if logger is not None: # pragma: no cover
|
|
259
|
+
to_logger(logger).info("Restore from %r finished after %s", str(path), timer)
|
|
260
|
+
return True # pragma: no cover
|
|
150
261
|
|
|
151
262
|
|
|
152
263
|
##
|
|
153
264
|
|
|
154
265
|
|
|
155
|
-
|
|
266
|
+
def _build_pg_restore_or_psql(
|
|
156
267
|
url: URL,
|
|
157
268
|
path: PathLike,
|
|
158
269
|
/,
|
|
159
270
|
*,
|
|
160
|
-
|
|
161
|
-
docker: str | None = None,
|
|
271
|
+
psql: bool = False,
|
|
162
272
|
data_only: bool = False,
|
|
273
|
+
clean: bool = False,
|
|
274
|
+
create: bool = False,
|
|
163
275
|
jobs: int | None = None,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
) ->
|
|
276
|
+
schema: MaybeCollectionStr | None = None,
|
|
277
|
+
schema_exc: MaybeCollectionStr | None = None,
|
|
278
|
+
table: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
279
|
+
role: str | None = None,
|
|
280
|
+
docker_container: str | None = None,
|
|
281
|
+
) -> str:
|
|
282
|
+
path = Path(path)
|
|
283
|
+
if (path.suffix == ".sql") or psql:
|
|
284
|
+
return _build_psql(url, path, docker_container=docker_container)
|
|
285
|
+
return _build_pg_restore(
|
|
286
|
+
url,
|
|
287
|
+
path,
|
|
288
|
+
data_only=data_only,
|
|
289
|
+
clean=clean,
|
|
290
|
+
create=create,
|
|
291
|
+
jobs=jobs,
|
|
292
|
+
schemas=schema,
|
|
293
|
+
schemas_exc=schema_exc,
|
|
294
|
+
tables=table,
|
|
295
|
+
role=role,
|
|
296
|
+
docker_container=docker_container,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _build_pg_restore(
|
|
301
|
+
url: URL,
|
|
302
|
+
path: PathLike,
|
|
303
|
+
/,
|
|
304
|
+
*,
|
|
305
|
+
data_only: bool = False,
|
|
306
|
+
clean: bool = False,
|
|
307
|
+
create: bool = False,
|
|
308
|
+
jobs: int | None = None,
|
|
309
|
+
schemas: MaybeCollectionStr | None = None,
|
|
310
|
+
schemas_exc: MaybeCollectionStr | None = None,
|
|
311
|
+
tables: MaybeCollection[TableOrORMInstOrClass | str] | None = None,
|
|
312
|
+
role: str | None = None,
|
|
313
|
+
docker_container: str | None = None,
|
|
314
|
+
) -> str:
|
|
170
315
|
"""Run `pg_restore`."""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
...
|
|
176
|
-
case None, None:
|
|
177
|
-
raise _PGRestoreDatabaseError(url=url)
|
|
178
|
-
case _ as never:
|
|
179
|
-
assert_never(never)
|
|
180
|
-
if url.host is None:
|
|
181
|
-
raise _PGRestoreHostError(url=url)
|
|
182
|
-
if url.port is None:
|
|
183
|
-
raise _PGRestorePortError(url=url)
|
|
184
|
-
parts: list[str] = []
|
|
185
|
-
if docker is not None:
|
|
186
|
-
parts.extend(["docker", "exec", "-it", docker])
|
|
316
|
+
extracted = extract_url(url)
|
|
317
|
+
parts: list[str] = ["pg_restore"]
|
|
318
|
+
if docker_container is not None:
|
|
319
|
+
parts = docker_exec_cmd(docker_container, *parts, PGPASSWORD=extracted.password)
|
|
187
320
|
parts.extend([
|
|
188
|
-
"pg_restore",
|
|
189
321
|
# general options
|
|
190
|
-
f"--dbname={database_use}",
|
|
191
322
|
"--verbose",
|
|
192
323
|
# restore options
|
|
324
|
+
*_resolve_data_only_and_clean(data_only=data_only, clean=clean),
|
|
193
325
|
"--exit-on-error",
|
|
194
326
|
"--no-owner",
|
|
195
327
|
"--no-privileges",
|
|
196
328
|
# connection options
|
|
197
|
-
f"--host={
|
|
198
|
-
f"--port={
|
|
329
|
+
f"--host={extracted.host}",
|
|
330
|
+
f"--port={extracted.port}",
|
|
331
|
+
f"--username={extracted.username}",
|
|
332
|
+
f"--dbname={extracted.database}",
|
|
199
333
|
"--no-password",
|
|
200
334
|
])
|
|
201
|
-
if
|
|
202
|
-
parts.append("--
|
|
203
|
-
else:
|
|
204
|
-
parts.extend(["--clean", "--if-exists"])
|
|
335
|
+
if create:
|
|
336
|
+
parts.append("--create")
|
|
205
337
|
if jobs is not None:
|
|
206
338
|
parts.append(f"--jobs={jobs}")
|
|
207
339
|
if schemas is not None:
|
|
@@ -210,77 +342,67 @@ async def pg_restore(
|
|
|
210
342
|
parts.extend([f"--exclude-schema={s}" for s in always_iterable(schemas_exc)])
|
|
211
343
|
if tables is not None:
|
|
212
344
|
parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(tables)])
|
|
213
|
-
if
|
|
214
|
-
parts.append(f"--
|
|
345
|
+
if role is not None:
|
|
346
|
+
parts.append(f"--role={role}")
|
|
215
347
|
parts.append(str(path))
|
|
216
|
-
|
|
217
|
-
if dry_run:
|
|
218
|
-
if logger is not None:
|
|
219
|
-
get_logger(logger=logger).info("Would run %r", str(cmd))
|
|
220
|
-
return
|
|
221
|
-
with temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
|
|
222
|
-
try:
|
|
223
|
-
output = await stream_command(cmd)
|
|
224
|
-
except KeyboardInterrupt:
|
|
225
|
-
if logger is not None:
|
|
226
|
-
get_logger(logger=logger).info(
|
|
227
|
-
"Cancelled restore from %r after %s", str(path), timer
|
|
228
|
-
)
|
|
229
|
-
else:
|
|
230
|
-
match output.return_code:
|
|
231
|
-
case 0:
|
|
232
|
-
if logger is not None:
|
|
233
|
-
get_logger(logger=logger).info(
|
|
234
|
-
"Restore from %r finished after %s", str(path), timer
|
|
235
|
-
)
|
|
236
|
-
case _:
|
|
237
|
-
if logger is not None:
|
|
238
|
-
get_logger(logger=logger).exception(
|
|
239
|
-
"Restore from %r failed after %s\nstderr:\n%s",
|
|
240
|
-
str(path),
|
|
241
|
-
timer,
|
|
242
|
-
output.stderr,
|
|
243
|
-
)
|
|
348
|
+
return " ".join(parts)
|
|
244
349
|
|
|
245
350
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
@dataclass(kw_only=True, slots=True)
|
|
266
|
-
class _PGRestorePortError(PGRestoreError):
|
|
267
|
-
@override
|
|
268
|
-
def __str__(self) -> str:
|
|
269
|
-
return f"Expected URL to contain a 'port'; got {self.url}"
|
|
351
|
+
def _build_psql(
|
|
352
|
+
url: URL, path: PathLike, /, *, docker_container: str | None = None
|
|
353
|
+
) -> str:
|
|
354
|
+
"""Run `psql`."""
|
|
355
|
+
extracted = extract_url(url)
|
|
356
|
+
parts: list[str] = ["psql"]
|
|
357
|
+
if docker_container is not None:
|
|
358
|
+
parts = docker_exec_cmd(docker_container, *parts, PGPASSWORD=extracted.password)
|
|
359
|
+
parts.extend([
|
|
360
|
+
# general options
|
|
361
|
+
f"--dbname={extracted.database}",
|
|
362
|
+
f"--file={str(path)!r}",
|
|
363
|
+
# connection options
|
|
364
|
+
f"--host={extracted.host}",
|
|
365
|
+
f"--port={extracted.port}",
|
|
366
|
+
f"--username={extracted.username}",
|
|
367
|
+
"--no-password",
|
|
368
|
+
])
|
|
369
|
+
return " ".join(parts)
|
|
270
370
|
|
|
271
371
|
|
|
272
372
|
##
|
|
273
373
|
|
|
274
374
|
|
|
275
375
|
def _get_table_name(obj: TableOrORMInstOrClass | str, /) -> str:
|
|
276
|
-
"""Get the table name from a Table or mapped class."""
|
|
277
376
|
match obj:
|
|
278
377
|
case Table() | DeclarativeBase() | type() as table_or_orm:
|
|
279
378
|
return get_table_name(table_or_orm)
|
|
280
379
|
case str() as name:
|
|
281
380
|
return name
|
|
282
|
-
case
|
|
381
|
+
case never:
|
|
382
|
+
assert_never(never)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _resolve_data_only_and_clean(
|
|
386
|
+
*, data_only: bool = False, clean: bool = False
|
|
387
|
+
) -> list[str]:
|
|
388
|
+
match data_only, clean:
|
|
389
|
+
case False, False:
|
|
390
|
+
return []
|
|
391
|
+
case True, False:
|
|
392
|
+
return ["--data-only"]
|
|
393
|
+
case False, True:
|
|
394
|
+
return ["--clean", "--if-exists"]
|
|
395
|
+
case True, True:
|
|
396
|
+
raise _ResolveDataOnlyAndCleanError
|
|
397
|
+
case never:
|
|
283
398
|
assert_never(never)
|
|
284
399
|
|
|
285
400
|
|
|
286
|
-
|
|
401
|
+
@dataclass(kw_only=True, slots=True)
|
|
402
|
+
class _ResolveDataOnlyAndCleanError(Exception):
|
|
403
|
+
@override
|
|
404
|
+
def __str__(self) -> str:
|
|
405
|
+
return "Cannot use '--data-only' and '--clean' together"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
__all__ = ["pg_dump", "restore"]
|