Add auth foundation and app DB management
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
APP_BASELINE_REVISION = "20260420_03_app_auth_baseline"
|
||||
|
||||
|
||||
class AppDatabaseAdoptionError(RuntimeError):
|
||||
"""Raised when the app database is missing or not managed as expected."""
|
||||
|
||||
|
||||
def _database_path_from_url(database_url: str) -> Path:
|
||||
prefix = "sqlite:///"
|
||||
if not database_url.startswith(prefix):
|
||||
raise AppDatabaseAdoptionError(
|
||||
f"Only sqlite URLs are supported for app DB initialization, got: {database_url}"
|
||||
)
|
||||
return Path(database_url[len(prefix) :])
|
||||
|
||||
|
||||
def _make_alembic_config(database_url: str) -> Config:
|
||||
config = Config("alembic_app.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
|
||||
def _alembic_version_table_exists(database_path: Path) -> bool:
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'alembic_version'"
|
||||
).fetchone()
|
||||
return row is not None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _fetch_alembic_revision(database_path: Path) -> str:
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
row = conn.execute("SELECT version_num FROM alembic_version").fetchone()
|
||||
if row is None:
|
||||
raise AppDatabaseAdoptionError("Alembic version table exists but contains no revision")
|
||||
return row[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _list_user_tables(database_path: Path) -> list[str]:
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
"""
|
||||
).fetchall()
|
||||
return sorted(row[0] for row in rows)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def validate_app_runtime_db(database_url: str) -> None:
|
||||
database_path = _database_path_from_url(database_url)
|
||||
if not database_path.exists():
|
||||
raise AppDatabaseAdoptionError(
|
||||
"App DB file was not found. Run 'python scripts/app_db_adopt.py' first to "
|
||||
"initialize the app DB before starting the app."
|
||||
)
|
||||
|
||||
if not _alembic_version_table_exists(database_path):
|
||||
raise AppDatabaseAdoptionError(
|
||||
"App DB exists but is not yet Alembic-managed. Run "
|
||||
"'python scripts/app_db_adopt.py' first before starting the app."
|
||||
)
|
||||
|
||||
current_revision = _fetch_alembic_revision(database_path)
|
||||
if current_revision != APP_BASELINE_REVISION:
|
||||
raise AppDatabaseAdoptionError(
|
||||
"App DB revision mismatch. Refusing to start the app: "
|
||||
f"expected {APP_BASELINE_REVISION}, got {current_revision}"
|
||||
)
|
||||
|
||||
|
||||
def adopt_or_initialize_app_db(database_url: str) -> str:
|
||||
database_path = _database_path_from_url(database_url)
|
||||
alembic_config = _make_alembic_config(database_url)
|
||||
|
||||
if database_path.exists():
|
||||
if _alembic_version_table_exists(database_path):
|
||||
current_revision = _fetch_alembic_revision(database_path)
|
||||
if current_revision != APP_BASELINE_REVISION:
|
||||
raise AppDatabaseAdoptionError(
|
||||
"App DB is already Alembic-managed but revision does not match "
|
||||
f"the expected baseline: expected {APP_BASELINE_REVISION}, "
|
||||
f"got {current_revision}"
|
||||
)
|
||||
return "already_managed"
|
||||
|
||||
existing_tables = _list_user_tables(database_path)
|
||||
if existing_tables:
|
||||
raise AppDatabaseAdoptionError(
|
||||
"App DB exists with unmanaged tables. Refusing to continue because there is "
|
||||
"no legacy app DB adoption path in this revision."
|
||||
)
|
||||
|
||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
command.upgrade(alembic_config, "head")
|
||||
return "initialized"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
settings = get_settings()
|
||||
result = adopt_or_initialize_app_db(settings.app_database_url)
|
||||
if result == "initialized":
|
||||
print("Initialized a new app DB via Alembic upgrade head.")
|
||||
else:
|
||||
print("App DB is already Alembic-managed at the expected baseline revision.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user