Update admin theme/layout and refresh README details.
Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Docker entrypoint: wait for DB, reconcile Alembic state, migrate, start API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
|
||||
from app.core.install_secrets import ensure_install_secrets
|
||||
from app.core.config import settings
|
||||
|
||||
INITIAL_REVISION = "20260711_0001"
|
||||
SCHEMA_TABLES = (
|
||||
"content_pages",
|
||||
"email_verification_tokens",
|
||||
"password_reset_tokens",
|
||||
"refresh_tokens",
|
||||
"user_profiles",
|
||||
"users",
|
||||
)
|
||||
|
||||
|
||||
def wait_for_database(max_attempts: int = 30, delay_seconds: float = 1.0):
|
||||
engine = create_engine(settings.database_url)
|
||||
last_error: Exception | None = None
|
||||
for _ in range(max_attempts):
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
connection.execute(text("SELECT 1"))
|
||||
return engine
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
time.sleep(delay_seconds)
|
||||
detail = str(last_error or "unknown error")
|
||||
hint = ""
|
||||
if "password authentication failed" in detail or "does not exist" in detail:
|
||||
hint = (
|
||||
"\n\nPostgres credentials in install.env do not match the existing database volume "
|
||||
"(common if docker compose ran before bootstrap_install.py).\n"
|
||||
"Local dev fix — deletes Docker DB data:\n"
|
||||
" docker compose --profile docker-web down -v\n"
|
||||
" docker compose --profile docker-web up -d --build\n"
|
||||
"See docs/secrets-recovery.md"
|
||||
)
|
||||
raise RuntimeError(f"Database is unavailable: {detail}{hint}") from last_error
|
||||
|
||||
|
||||
def current_revision(engine) -> str | None:
|
||||
inspector = inspect(engine)
|
||||
if "alembic_version" not in inspector.get_table_names():
|
||||
return None
|
||||
with engine.connect() as connection:
|
||||
return connection.execute(text("SELECT version_num FROM alembic_version")).scalar()
|
||||
|
||||
|
||||
def _reset_schema(engine) -> None:
|
||||
cascade = " CASCADE" if engine.dialect.name == "postgresql" else ""
|
||||
with engine.begin() as connection:
|
||||
for table in SCHEMA_TABLES:
|
||||
connection.execute(text(f'DROP TABLE IF EXISTS "{table}"{cascade}'))
|
||||
connection.execute(text(f'DROP TABLE IF EXISTS "alembic_version"{cascade}'))
|
||||
|
||||
|
||||
def run_migrations(engine) -> None:
|
||||
config = Config("alembic.ini")
|
||||
tables = set(inspect(engine).get_table_names())
|
||||
revision = current_revision(engine)
|
||||
schema_tables = set(SCHEMA_TABLES)
|
||||
existing_schema = tables & schema_tables
|
||||
|
||||
if existing_schema:
|
||||
if schema_tables.issubset(tables):
|
||||
if revision is None:
|
||||
command.stamp(config, INITIAL_REVISION)
|
||||
else:
|
||||
_reset_schema(engine)
|
||||
|
||||
command.upgrade(config, "head")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ensure_install_secrets()
|
||||
engine = wait_for_database()
|
||||
run_migrations(engine)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user