dploydb 0.1.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.
- dploydb/__init__.py +1 -0
- dploydb/__main__.py +6 -0
- dploydb/backup.py +455 -0
- dploydb/candidate.py +868 -0
- dploydb/cli.py +1111 -0
- dploydb/config.py +784 -0
- dploydb/cutover.py +326 -0
- dploydb/deploy.py +1163 -0
- dploydb/deployment_dependencies.py +366 -0
- dploydb/deployment_evidence.py +136 -0
- dploydb/diagnostics.py +1020 -0
- dploydb/errors.py +140 -0
- dploydb/health.py +625 -0
- dploydb/locking.py +609 -0
- dploydb/manual_restore.py +825 -0
- dploydb/migration.py +575 -0
- dploydb/models.py +927 -0
- dploydb/recovery.py +1210 -0
- dploydb/redaction.py +229 -0
- dploydb/releases.py +611 -0
- dploydb/restore.py +461 -0
- dploydb/retention.py +165 -0
- dploydb/runners/__init__.py +33 -0
- dploydb/runners/base.py +389 -0
- dploydb/runners/docker_compose.py +590 -0
- dploydb/runners/docker_compose_production.py +966 -0
- dploydb/sqlite_checks.py +136 -0
- dploydb/state.py +604 -0
- dploydb/storage/__init__.py +13 -0
- dploydb/storage/base.py +52 -0
- dploydb/storage/local.py +301 -0
- dploydb/storage/s3.py +737 -0
- dploydb/subprocesses.py +641 -0
- dploydb/traffic.py +165 -0
- dploydb-0.1.0.dist-info/METADATA +583 -0
- dploydb-0.1.0.dist-info/RECORD +40 -0
- dploydb-0.1.0.dist-info/WHEEL +4 -0
- dploydb-0.1.0.dist-info/entry_points.txt +2 -0
- dploydb-0.1.0.dist-info/licenses/LICENSE +201 -0
- dploydb-0.1.0.dist-info/licenses/NOTICE +4 -0
dploydb/sqlite_checks.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Bounded, read-only SQLite safety checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
import stat
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Final, Literal
|
|
12
|
+
|
|
13
|
+
from dploydb.errors import SafetyCheckError
|
|
14
|
+
from dploydb.models import SQLiteVerification, utc_now
|
|
15
|
+
|
|
16
|
+
DEFAULT_SQLITE_TIMEOUT_SECONDS: Final[float] = 10.0
|
|
17
|
+
PROGRESS_INSTRUCTIONS: Final[int] = 1_000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def verify_sqlite_database(
|
|
21
|
+
path: Path,
|
|
22
|
+
*,
|
|
23
|
+
deep: bool = False,
|
|
24
|
+
timeout_seconds: float = DEFAULT_SQLITE_TIMEOUT_SECONDS,
|
|
25
|
+
monotonic: Callable[[], float] = time.monotonic,
|
|
26
|
+
progress_instructions: int = PROGRESS_INSTRUCTIONS,
|
|
27
|
+
) -> SQLiteVerification:
|
|
28
|
+
"""Open one database read-only and require all configured integrity checks."""
|
|
29
|
+
if timeout_seconds <= 0:
|
|
30
|
+
raise ValueError("SQLite verification timeout must be positive")
|
|
31
|
+
if progress_instructions <= 0:
|
|
32
|
+
raise ValueError("SQLite progress interval must be positive")
|
|
33
|
+
|
|
34
|
+
started = monotonic()
|
|
35
|
+
deadline = started + timeout_seconds
|
|
36
|
+
before = _validate_database_file(path)
|
|
37
|
+
connection: sqlite3.Connection | None = None
|
|
38
|
+
timed_out = False
|
|
39
|
+
|
|
40
|
+
def progress() -> int:
|
|
41
|
+
nonlocal timed_out
|
|
42
|
+
timed_out = monotonic() >= deadline
|
|
43
|
+
return 1 if timed_out else 0
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
remaining = deadline - monotonic()
|
|
47
|
+
if remaining <= 0:
|
|
48
|
+
raise TimeoutError
|
|
49
|
+
connection = sqlite3.connect(
|
|
50
|
+
f"{path.as_uri()}?mode=ro",
|
|
51
|
+
uri=True,
|
|
52
|
+
timeout=remaining,
|
|
53
|
+
isolation_level=None,
|
|
54
|
+
)
|
|
55
|
+
busy_milliseconds = max(1, min(2_147_483_647, int(remaining * 1_000)))
|
|
56
|
+
connection.execute(f"PRAGMA busy_timeout = {busy_milliseconds}")
|
|
57
|
+
connection.set_progress_handler(progress, progress_instructions)
|
|
58
|
+
|
|
59
|
+
quick_rows = connection.execute("PRAGMA quick_check(1)").fetchall()
|
|
60
|
+
if quick_rows != [("ok",)]:
|
|
61
|
+
detail = _bounded_check_detail(quick_rows)
|
|
62
|
+
raise _check_error(path, f"SQLite quick_check failed: {detail}")
|
|
63
|
+
|
|
64
|
+
foreign_key_row = connection.execute("PRAGMA foreign_key_check").fetchone()
|
|
65
|
+
if foreign_key_row is not None:
|
|
66
|
+
detail = _bounded_check_detail([foreign_key_row])
|
|
67
|
+
raise _check_error(path, f"SQLite foreign_key_check failed: {detail}")
|
|
68
|
+
|
|
69
|
+
integrity_passed: Literal[True] | None = None
|
|
70
|
+
if deep:
|
|
71
|
+
integrity_rows = connection.execute("PRAGMA integrity_check(1)").fetchall()
|
|
72
|
+
if integrity_rows != [("ok",)]:
|
|
73
|
+
detail = _bounded_check_detail(integrity_rows)
|
|
74
|
+
raise _check_error(path, f"SQLite integrity_check failed: {detail}")
|
|
75
|
+
integrity_passed = True
|
|
76
|
+
except TimeoutError:
|
|
77
|
+
raise _timeout_error(path, timeout_seconds) from None
|
|
78
|
+
except sqlite3.Error as exc:
|
|
79
|
+
if timed_out or monotonic() >= deadline:
|
|
80
|
+
raise _timeout_error(path, timeout_seconds) from None
|
|
81
|
+
raise _check_error(path, f"SQLite verification could not complete: {exc}") from None
|
|
82
|
+
finally:
|
|
83
|
+
if connection is not None:
|
|
84
|
+
connection.set_progress_handler(None, 0)
|
|
85
|
+
connection.close()
|
|
86
|
+
|
|
87
|
+
after = _validate_database_file(path)
|
|
88
|
+
if (before.st_dev, before.st_ino) != (
|
|
89
|
+
after.st_dev,
|
|
90
|
+
after.st_ino,
|
|
91
|
+
):
|
|
92
|
+
raise _check_error(path, "database changed identity during verification")
|
|
93
|
+
|
|
94
|
+
return SQLiteVerification(
|
|
95
|
+
quick_check_passed=True,
|
|
96
|
+
foreign_key_check_passed=True,
|
|
97
|
+
integrity_check_passed=integrity_passed,
|
|
98
|
+
checked_at=utc_now(),
|
|
99
|
+
duration_seconds=max(0.0, monotonic() - started),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _validate_database_file(path: Path) -> os.stat_result:
|
|
104
|
+
try:
|
|
105
|
+
details = path.lstat()
|
|
106
|
+
except OSError as exc:
|
|
107
|
+
raise _check_error(path, f"database file could not be inspected: {exc}") from None
|
|
108
|
+
if path.is_symlink() or not stat.S_ISREG(details.st_mode):
|
|
109
|
+
raise _check_error(path, "database path must be a regular non-symlink file")
|
|
110
|
+
if not os.access(path, os.R_OK):
|
|
111
|
+
raise _check_error(path, "database file is not readable")
|
|
112
|
+
return details
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _bounded_check_detail(rows: list[tuple[object, ...]]) -> str:
|
|
116
|
+
if not rows:
|
|
117
|
+
return "check returned no result"
|
|
118
|
+
text = repr(rows[0])
|
|
119
|
+
return text if len(text) <= 512 else f"{text[:512]}..."
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _check_error(path: Path, detail: str) -> SafetyCheckError:
|
|
123
|
+
return SafetyCheckError(
|
|
124
|
+
detail,
|
|
125
|
+
production_changed=False,
|
|
126
|
+
previous_application_running=None,
|
|
127
|
+
log_path=path,
|
|
128
|
+
next_safe_action="Repair or replace the database from a verified backup, then retry.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _timeout_error(path: Path, timeout_seconds: float) -> SafetyCheckError:
|
|
133
|
+
return _check_error(
|
|
134
|
+
path,
|
|
135
|
+
f"SQLite verification timed out after {timeout_seconds:g} seconds",
|
|
136
|
+
)
|