plain.models 0.49.2__py3-none-any.whl → 0.51.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.
- plain/models/CHANGELOG.md +27 -0
- plain/models/README.md +26 -42
- plain/models/__init__.py +2 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +24 -14
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +37 -20
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +338 -218
- plain/models/backends/base/validation.py +13 -4
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +13 -4
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +107 -40
- plain/models/backends/mysql/schema.py +52 -28
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +110 -43
- plain/models/backends/postgresql/schema.py +88 -49
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +126 -43
- plain/models/backends/sqlite3/schema.py +127 -92
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +221 -229
- plain/models/cli.py +98 -67
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +79 -56
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +80 -56
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +441 -258
- plain/models/fields/__init__.py +272 -217
- plain/models/fields/json.py +123 -57
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +324 -290
- plain/models/fields/related_descriptors.py +33 -24
- plain/models/fields/related_lookups.py +24 -12
- plain/models/fields/related_managers.py +102 -79
- plain/models/fields/reverse_related.py +66 -63
- plain/models/forms.py +101 -75
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +57 -32
- plain/models/lookups.py +228 -153
- plain/models/meta.py +505 -0
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +38 -20
- plain/models/migrations/operations/fields.py +95 -48
- plain/models/migrations/operations/models.py +246 -142
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +27 -16
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +232 -156
- plain/models/migrations/utils.py +30 -14
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +189 -518
- plain/models/otel.py +16 -6
- plain/models/preflight.py +42 -17
- plain/models/query.py +400 -251
- plain/models/query_utils.py +109 -69
- plain/models/registry.py +40 -21
- plain/models/sql/compiler.py +190 -127
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +320 -225
- plain/models/sql/subqueries.py +36 -25
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +17 -6
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
- plain_models-0.51.0.dist-info/RECORD +123 -0
- plain_models-0.49.2.dist-info/RECORD +0 -122
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
plain/models/backends/utils.py
CHANGED
@@ -1,52 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import datetime
|
2
4
|
import decimal
|
3
5
|
import functools
|
4
6
|
import logging
|
5
7
|
import time
|
8
|
+
from collections.abc import Generator, Iterator
|
6
9
|
from contextlib import contextmanager
|
7
10
|
from hashlib import md5
|
11
|
+
from typing import TYPE_CHECKING, Any
|
8
12
|
|
9
13
|
from plain.models.db import NotSupportedError
|
10
14
|
from plain.models.otel import db_span
|
11
15
|
from plain.utils.dateparse import parse_time
|
12
16
|
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
19
|
+
|
13
20
|
logger = logging.getLogger("plain.models.backends")
|
14
21
|
|
15
22
|
|
16
23
|
class CursorWrapper:
|
17
|
-
def __init__(self, cursor, db):
|
24
|
+
def __init__(self, cursor: Any, db: Any) -> None:
|
18
25
|
self.cursor = cursor
|
19
26
|
self.db = db
|
20
27
|
|
21
28
|
WRAP_ERROR_ATTRS = frozenset(["fetchone", "fetchmany", "fetchall", "nextset"])
|
22
29
|
|
23
|
-
def __getattr__(self, attr):
|
30
|
+
def __getattr__(self, attr: str) -> Any:
|
24
31
|
cursor_attr = getattr(self.cursor, attr)
|
25
32
|
if attr in CursorWrapper.WRAP_ERROR_ATTRS:
|
26
33
|
return self.db.wrap_database_errors(cursor_attr)
|
27
34
|
else:
|
28
35
|
return cursor_attr
|
29
36
|
|
30
|
-
def __iter__(self):
|
37
|
+
def __iter__(self) -> Iterator[Any]:
|
31
38
|
with self.db.wrap_database_errors:
|
32
39
|
yield from self.cursor
|
33
40
|
|
34
|
-
def __enter__(self):
|
41
|
+
def __enter__(self) -> CursorWrapper:
|
35
42
|
return self
|
36
43
|
|
37
|
-
def __exit__(self, type, value, traceback):
|
44
|
+
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
|
38
45
|
# Close instead of passing through to avoid backend-specific behavior
|
39
46
|
# (#17671). Catch errors liberally because errors in cleanup code
|
40
47
|
# aren't useful.
|
41
48
|
try:
|
42
|
-
self.close()
|
49
|
+
self.close() # type: ignore[attr-defined]
|
43
50
|
except self.db.Database.Error:
|
44
51
|
pass
|
45
52
|
|
46
53
|
# The following methods cannot be implemented in __getattr__, because the
|
47
54
|
# code must run when the method is invoked, not just when it is accessed.
|
48
55
|
|
49
|
-
def callproc(self, procname, params=None, kparams=None):
|
56
|
+
def callproc(self, procname: str, params: Any = None, kparams: Any = None) -> Any:
|
50
57
|
# Keyword parameters for callproc aren't supported in PEP 249, but the
|
51
58
|
# database driver may support them (e.g. cx_Oracle).
|
52
59
|
if kparams is not None and not self.db.features.supports_callproc_kwargs:
|
@@ -64,23 +71,25 @@ class CursorWrapper:
|
|
64
71
|
params = params or ()
|
65
72
|
return self.cursor.callproc(procname, params, kparams)
|
66
73
|
|
67
|
-
def execute(self, sql, params=None):
|
74
|
+
def execute(self, sql: str, params: Any = None) -> Any:
|
68
75
|
return self._execute_with_wrappers(
|
69
76
|
sql, params, many=False, executor=self._execute
|
70
77
|
)
|
71
78
|
|
72
|
-
def executemany(self, sql, param_list):
|
79
|
+
def executemany(self, sql: str, param_list: Any) -> Any:
|
73
80
|
return self._execute_with_wrappers(
|
74
81
|
sql, param_list, many=True, executor=self._executemany
|
75
82
|
)
|
76
83
|
|
77
|
-
def _execute_with_wrappers(
|
78
|
-
|
84
|
+
def _execute_with_wrappers(
|
85
|
+
self, sql: str, params: Any, many: bool, executor: Any
|
86
|
+
) -> Any:
|
87
|
+
context: dict[str, Any] = {"connection": self.db, "cursor": self}
|
79
88
|
for wrapper in reversed(self.db.execute_wrappers):
|
80
89
|
executor = functools.partial(wrapper, executor)
|
81
90
|
return executor(sql, params, many, context)
|
82
91
|
|
83
|
-
def _execute(self, sql, params, *ignored_wrapper_args):
|
92
|
+
def _execute(self, sql: str, params: Any, *ignored_wrapper_args: Any) -> Any:
|
84
93
|
# Wrap in an OpenTelemetry span with standard attributes.
|
85
94
|
with db_span(self.db, sql, params=params):
|
86
95
|
self.db.validate_no_broken_transaction()
|
@@ -90,7 +99,9 @@ class CursorWrapper:
|
|
90
99
|
else:
|
91
100
|
return self.cursor.execute(sql, params)
|
92
101
|
|
93
|
-
def _executemany(
|
102
|
+
def _executemany(
|
103
|
+
self, sql: str, param_list: Any, *ignored_wrapper_args: Any
|
104
|
+
) -> Any:
|
94
105
|
with db_span(self.db, sql, many=True, params=param_list):
|
95
106
|
self.db.validate_no_broken_transaction()
|
96
107
|
with self.db.wrap_database_errors:
|
@@ -100,18 +111,22 @@ class CursorWrapper:
|
|
100
111
|
class CursorDebugWrapper(CursorWrapper):
|
101
112
|
# XXX callproc isn't instrumented at this time.
|
102
113
|
|
103
|
-
def execute(self, sql, params=None):
|
114
|
+
def execute(self, sql: str, params: Any = None) -> Any:
|
104
115
|
with self.debug_sql(sql, params, use_last_executed_query=True):
|
105
116
|
return super().execute(sql, params)
|
106
117
|
|
107
|
-
def executemany(self, sql, param_list):
|
118
|
+
def executemany(self, sql: str, param_list: Any) -> Any:
|
108
119
|
with self.debug_sql(sql, param_list, many=True):
|
109
120
|
return super().executemany(sql, param_list)
|
110
121
|
|
111
122
|
@contextmanager
|
112
123
|
def debug_sql(
|
113
|
-
self,
|
114
|
-
|
124
|
+
self,
|
125
|
+
sql: str | None = None,
|
126
|
+
params: Any = None,
|
127
|
+
use_last_executed_query: bool = False,
|
128
|
+
many: bool = False,
|
129
|
+
) -> Generator[None, None, None]:
|
115
130
|
start = time.monotonic()
|
116
131
|
try:
|
117
132
|
yield
|
@@ -121,7 +136,7 @@ class CursorDebugWrapper(CursorWrapper):
|
|
121
136
|
if use_last_executed_query:
|
122
137
|
sql = self.db.ops.last_executed_query(self.cursor, sql, params)
|
123
138
|
try:
|
124
|
-
times = len(params) if many else ""
|
139
|
+
times = len(params) if many else "" # type: ignore[arg-type]
|
125
140
|
except TypeError:
|
126
141
|
# params could be an iterator.
|
127
142
|
times = "?"
|
@@ -145,7 +160,9 @@ class CursorDebugWrapper(CursorWrapper):
|
|
145
160
|
|
146
161
|
|
147
162
|
@contextmanager
|
148
|
-
def debug_transaction(
|
163
|
+
def debug_transaction(
|
164
|
+
connection: BaseDatabaseWrapper, sql: str
|
165
|
+
) -> Generator[None, None, None]:
|
149
166
|
start = time.monotonic()
|
150
167
|
try:
|
151
168
|
yield
|
@@ -171,7 +188,7 @@ def debug_transaction(connection, sql):
|
|
171
188
|
)
|
172
189
|
|
173
190
|
|
174
|
-
def split_tzname_delta(tzname):
|
191
|
+
def split_tzname_delta(tzname: str) -> tuple[str, str | None, str | None]:
|
175
192
|
"""
|
176
193
|
Split a time zone name into a 3-tuple of (name, sign, offset).
|
177
194
|
"""
|
@@ -188,13 +205,15 @@ def split_tzname_delta(tzname):
|
|
188
205
|
###############################################
|
189
206
|
|
190
207
|
|
191
|
-
def typecast_date(s):
|
208
|
+
def typecast_date(s: str | None) -> datetime.date | None:
|
192
209
|
return (
|
193
210
|
datetime.date(*map(int, s.split("-"))) if s else None
|
194
211
|
) # return None if s is null
|
195
212
|
|
196
213
|
|
197
|
-
def typecast_time(
|
214
|
+
def typecast_time(
|
215
|
+
s: str | None,
|
216
|
+
) -> datetime.time | None: # does NOT store time zone information
|
198
217
|
if not s:
|
199
218
|
return None
|
200
219
|
hour, minutes, seconds = s.split(":")
|
@@ -207,7 +226,9 @@ def typecast_time(s): # does NOT store time zone information
|
|
207
226
|
)
|
208
227
|
|
209
228
|
|
210
|
-
def typecast_timestamp(
|
229
|
+
def typecast_timestamp(
|
230
|
+
s: str | None,
|
231
|
+
) -> datetime.date | datetime.datetime | None: # does NOT store time zone information
|
211
232
|
# "2005-07-29 15:48:00.590358-05"
|
212
233
|
# "2005-07-29 09:56:00-05"
|
213
234
|
if not s:
|
@@ -243,7 +264,7 @@ def typecast_timestamp(s): # does NOT store time zone information
|
|
243
264
|
###############################################
|
244
265
|
|
245
266
|
|
246
|
-
def split_identifier(identifier):
|
267
|
+
def split_identifier(identifier: str) -> tuple[str, str]:
|
247
268
|
"""
|
248
269
|
Split an SQL identifier into a two element tuple of (namespace, name).
|
249
270
|
|
@@ -257,7 +278,7 @@ def split_identifier(identifier):
|
|
257
278
|
return namespace.strip('"'), name.strip('"')
|
258
279
|
|
259
280
|
|
260
|
-
def truncate_name(identifier, length=None, hash_len=4):
|
281
|
+
def truncate_name(identifier: str, length: int | None = None, hash_len: int = 4) -> str:
|
261
282
|
"""
|
262
283
|
Shorten an SQL identifier to a repeatable mangled version with the given
|
263
284
|
length.
|
@@ -278,7 +299,7 @@ def truncate_name(identifier, length=None, hash_len=4):
|
|
278
299
|
)
|
279
300
|
|
280
301
|
|
281
|
-
def names_digest(*args, length):
|
302
|
+
def names_digest(*args: str, length: int) -> str:
|
282
303
|
"""
|
283
304
|
Generate a 32-bit digest of a set of arguments that can be used to shorten
|
284
305
|
identifying names.
|
@@ -289,7 +310,9 @@ def names_digest(*args, length):
|
|
289
310
|
return h.hexdigest()[:length]
|
290
311
|
|
291
312
|
|
292
|
-
def format_number(
|
313
|
+
def format_number(
|
314
|
+
value: decimal.Decimal | None, max_digits: int | None, decimal_places: int | None
|
315
|
+
) -> str | None:
|
293
316
|
"""
|
294
317
|
Format a number into a string with the requisite number of digits and
|
295
318
|
decimal places.
|
@@ -304,12 +327,12 @@ def format_number(value, max_digits, decimal_places):
|
|
304
327
|
decimal.Decimal(1).scaleb(-decimal_places), context=context
|
305
328
|
)
|
306
329
|
else:
|
307
|
-
context.traps[decimal.Rounded] = 1
|
330
|
+
context.traps[decimal.Rounded] = 1 # type: ignore[assignment]
|
308
331
|
value = context.create_decimal(value)
|
309
332
|
return f"{value:f}"
|
310
333
|
|
311
334
|
|
312
|
-
def strip_quotes(table_name):
|
335
|
+
def strip_quotes(table_name: str) -> str:
|
313
336
|
"""
|
314
337
|
Strip quotes off of quoted table names to make them safe for use in index
|
315
338
|
names, sequence names, etc. For example '"USER"."TABLE"' (an Oracle naming
|
plain/models/backups/cli.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
2
4
|
import time
|
3
5
|
from pathlib import Path
|
@@ -11,13 +13,13 @@ from .core import DatabaseBackups
|
|
11
13
|
|
12
14
|
@register_cli("backups")
|
13
15
|
@click.group("backups")
|
14
|
-
def cli():
|
16
|
+
def cli() -> None:
|
15
17
|
"""Local database backups"""
|
16
18
|
pass
|
17
19
|
|
18
20
|
|
19
21
|
@cli.command("list")
|
20
|
-
def list_backups():
|
22
|
+
def list_backups() -> None:
|
21
23
|
backups_handler = DatabaseBackups()
|
22
24
|
backups = backups_handler.find_backups()
|
23
25
|
if not backups:
|
@@ -40,7 +42,7 @@ def list_backups():
|
|
40
42
|
@cli.command("create")
|
41
43
|
@click.option("--pg-dump", default="pg_dump", envvar="PG_DUMP")
|
42
44
|
@click.argument("backup_name", default="")
|
43
|
-
def create_backup(backup_name, pg_dump):
|
45
|
+
def create_backup(backup_name: str, pg_dump: str) -> None:
|
44
46
|
backups_handler = DatabaseBackups()
|
45
47
|
|
46
48
|
if not backup_name:
|
@@ -62,7 +64,7 @@ def create_backup(backup_name, pg_dump):
|
|
62
64
|
@click.option("--latest", is_flag=True)
|
63
65
|
@click.option("--pg-restore", default="pg_restore", envvar="PG_RESTORE")
|
64
66
|
@click.argument("backup_name", default="")
|
65
|
-
def restore_backup(backup_name, latest, pg_restore):
|
67
|
+
def restore_backup(backup_name: str, latest: bool, pg_restore: str) -> None:
|
66
68
|
backups_handler = DatabaseBackups()
|
67
69
|
|
68
70
|
if backup_name and latest:
|
@@ -89,7 +91,7 @@ def restore_backup(backup_name, latest, pg_restore):
|
|
89
91
|
|
90
92
|
@cli.command("delete")
|
91
93
|
@click.argument("backup_name")
|
92
|
-
def delete_backup(backup_name):
|
94
|
+
def delete_backup(backup_name: str) -> None:
|
93
95
|
backups_handler = DatabaseBackups()
|
94
96
|
try:
|
95
97
|
backups_handler.delete(backup_name)
|
@@ -101,7 +103,7 @@ def delete_backup(backup_name):
|
|
101
103
|
|
102
104
|
@cli.command("clear")
|
103
105
|
@click.confirmation_option(prompt="Are you sure you want to delete all backups?")
|
104
|
-
def clear_backups():
|
106
|
+
def clear_backups() -> None:
|
105
107
|
backups_handler = DatabaseBackups()
|
106
108
|
backups = backups_handler.find_backups()
|
107
109
|
for backup in backups:
|
plain/models/backups/clients.py
CHANGED
@@ -1,13 +1,20 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import gzip
|
2
4
|
import os
|
3
5
|
import subprocess
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
4
11
|
|
5
12
|
|
6
13
|
class PostgresBackupClient:
|
7
|
-
def __init__(self, connection):
|
14
|
+
def __init__(self, connection: BaseDatabaseWrapper) -> None:
|
8
15
|
self.connection = connection
|
9
16
|
|
10
|
-
def get_env(self):
|
17
|
+
def get_env(self) -> dict[str, str]:
|
11
18
|
settings_dict = self.connection.settings_dict
|
12
19
|
options = settings_dict.get("OPTIONS", {})
|
13
20
|
env = {}
|
@@ -27,7 +34,7 @@ class PostgresBackupClient:
|
|
27
34
|
env["PGSSLKEY"] = str(options.get("sslkey"))
|
28
35
|
return env
|
29
36
|
|
30
|
-
def create_backup(self, backup_path, *, pg_dump="pg_dump"):
|
37
|
+
def create_backup(self, backup_path: Path, *, pg_dump: str = "pg_dump") -> None:
|
31
38
|
settings_dict = self.connection.settings_dict
|
32
39
|
|
33
40
|
args = pg_dump.split()
|
@@ -64,7 +71,9 @@ class PostgresBackupClient:
|
|
64
71
|
cmd, env={**os.environ, **self.get_env()}, check=True, shell=True
|
65
72
|
)
|
66
73
|
|
67
|
-
def restore_backup(
|
74
|
+
def restore_backup(
|
75
|
+
self, backup_path: Path, *, pg_restore: str = "pg_restore", psql: str = "psql"
|
76
|
+
) -> None:
|
68
77
|
settings_dict = self.connection.settings_dict
|
69
78
|
|
70
79
|
host = settings_dict.get("HOST")
|
@@ -118,17 +127,17 @@ class PostgresBackupClient:
|
|
118
127
|
|
119
128
|
|
120
129
|
class SQLiteBackupClient:
|
121
|
-
def __init__(self, connection):
|
130
|
+
def __init__(self, connection: BaseDatabaseWrapper) -> None:
|
122
131
|
self.connection = connection
|
123
132
|
|
124
|
-
def create_backup(self, backup_path):
|
133
|
+
def create_backup(self, backup_path: Path) -> None:
|
125
134
|
self.connection.ensure_connection()
|
126
135
|
src_conn = self.connection.connection
|
127
136
|
dump = "\n".join(src_conn.iterdump())
|
128
137
|
with gzip.open(backup_path, "wt") as f:
|
129
138
|
f.write(dump)
|
130
139
|
|
131
|
-
def restore_backup(self, backup_path):
|
140
|
+
def restore_backup(self, backup_path: Path) -> None:
|
132
141
|
with gzip.open(backup_path, "rt") as f:
|
133
142
|
sql = f.read()
|
134
143
|
|
plain/models/backups/core.py
CHANGED
@@ -1,18 +1,29 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import datetime
|
2
4
|
import os
|
5
|
+
from collections.abc import Generator
|
3
6
|
from pathlib import Path
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
4
8
|
|
5
9
|
from plain.runtime import PLAIN_TEMP_PATH
|
6
10
|
|
7
|
-
from .. import db_connection
|
11
|
+
from .. import db_connection as _db_connection
|
8
12
|
from .clients import PostgresBackupClient, SQLiteBackupClient
|
9
13
|
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
16
|
+
|
17
|
+
db_connection = cast("BaseDatabaseWrapper", _db_connection)
|
18
|
+
else:
|
19
|
+
db_connection = _db_connection
|
20
|
+
|
10
21
|
|
11
22
|
class DatabaseBackups:
|
12
|
-
def __init__(self):
|
23
|
+
def __init__(self) -> None:
|
13
24
|
self.path = PLAIN_TEMP_PATH / "backups"
|
14
25
|
|
15
|
-
def find_backups(self):
|
26
|
+
def find_backups(self) -> list[DatabaseBackup]:
|
16
27
|
if not self.path.exists():
|
17
28
|
return []
|
18
29
|
|
@@ -27,20 +38,20 @@ class DatabaseBackups:
|
|
27
38
|
|
28
39
|
return backups
|
29
40
|
|
30
|
-
def create(self, name, **create_kwargs):
|
41
|
+
def create(self, name: str, **create_kwargs: Any) -> Path:
|
31
42
|
backup = DatabaseBackup(name, backups_path=self.path)
|
32
43
|
if backup.exists():
|
33
44
|
raise Exception(f"Backup {name} already exists")
|
34
45
|
backup_dir = backup.create(**create_kwargs)
|
35
46
|
return backup_dir
|
36
47
|
|
37
|
-
def restore(self, name, **restore_kwargs):
|
48
|
+
def restore(self, name: str, **restore_kwargs: Any) -> None:
|
38
49
|
backup = DatabaseBackup(name, backups_path=self.path)
|
39
50
|
if not backup.exists():
|
40
51
|
raise Exception(f"Backup {name} not found")
|
41
52
|
backup.restore(**restore_kwargs)
|
42
53
|
|
43
|
-
def delete(self, name):
|
54
|
+
def delete(self, name: str) -> None:
|
44
55
|
backup = DatabaseBackup(name, backups_path=self.path)
|
45
56
|
if not backup.exists():
|
46
57
|
raise Exception(f"Backup {name} not found")
|
@@ -48,17 +59,17 @@ class DatabaseBackups:
|
|
48
59
|
|
49
60
|
|
50
61
|
class DatabaseBackup:
|
51
|
-
def __init__(self, name: str, *, backups_path: Path):
|
62
|
+
def __init__(self, name: str, *, backups_path: Path) -> None:
|
52
63
|
self.name = name
|
53
64
|
self.path = backups_path / name
|
54
65
|
|
55
66
|
if not self.name:
|
56
67
|
raise ValueError("Backup name is required")
|
57
68
|
|
58
|
-
def exists(self):
|
69
|
+
def exists(self) -> bool:
|
59
70
|
return self.path.exists()
|
60
71
|
|
61
|
-
def create(self, **create_kwargs):
|
72
|
+
def create(self, **create_kwargs: Any) -> Path:
|
62
73
|
self.path.mkdir(parents=True, exist_ok=True)
|
63
74
|
|
64
75
|
backup_path = self.path / "default.backup"
|
@@ -75,7 +86,7 @@ class DatabaseBackup:
|
|
75
86
|
|
76
87
|
return self.path
|
77
88
|
|
78
|
-
def iter_files(self):
|
89
|
+
def iter_files(self) -> Generator[Path, None, None]:
|
79
90
|
for backup_file in self.path.iterdir():
|
80
91
|
if not backup_file.is_file():
|
81
92
|
continue
|
@@ -83,7 +94,7 @@ class DatabaseBackup:
|
|
83
94
|
continue
|
84
95
|
yield backup_file
|
85
96
|
|
86
|
-
def restore(self, **restore_kwargs):
|
97
|
+
def restore(self, **restore_kwargs: Any) -> None:
|
87
98
|
for backup_file in self.iter_files():
|
88
99
|
if db_connection.vendor == "postgresql":
|
89
100
|
PostgresBackupClient(db_connection).restore_backup(
|
@@ -95,12 +106,12 @@ class DatabaseBackup:
|
|
95
106
|
else:
|
96
107
|
raise Exception("Unsupported database vendor")
|
97
108
|
|
98
|
-
def delete(self):
|
109
|
+
def delete(self) -> None:
|
99
110
|
for backup_file in self.iter_files():
|
100
111
|
backup_file.unlink()
|
101
112
|
|
102
113
|
self.path.rmdir()
|
103
114
|
|
104
|
-
def updated_at(self):
|
115
|
+
def updated_at(self) -> datetime.datetime:
|
105
116
|
mtime = os.path.getmtime(self.path)
|
106
117
|
return datetime.datetime.fromtimestamp(mtime)
|