| """CLI utility to create or truncate the prediction log tables. |
| |
| Usage: |
| uv run python -m database.setup --create # both prod + test |
| uv run python -m database.setup --create-test # test table only |
| uv run python -m database.setup --truncate-test # cleanup CI artefacts |
| |
| DATABASE_URL is read from the environment (or ``database/.env`` for local |
| runs). The script is idempotent — re-running ``--create`` is a no-op when |
| the tables already exist. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import logging |
| import os |
| import sys |
| from pathlib import Path |
|
|
| from sqlalchemy import MetaData, create_engine, text |
|
|
| from database.models import ( |
| PROD_TABLE_NAME, |
| TEST_TABLE_NAME, |
| build_predictions_log_table, |
| ) |
|
|
| logger = logging.getLogger("database.setup") |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s %(message)s") |
|
|
|
|
| def _load_dotenv_fallback() -> None: |
| """Load database/.env if python-dotenv is installed and the file exists.""" |
| env_path = Path(__file__).parent / ".env" |
| if not env_path.exists(): |
| return |
| try: |
| from dotenv import load_dotenv |
| except ImportError: |
| logger.warning("python-dotenv not installed; relying on shell env only") |
| return |
| load_dotenv(env_path) |
|
|
|
|
| def _resolve_database_url(cli_url: str | None) -> str: |
| if cli_url: |
| return cli_url |
| url = os.getenv("DATABASE_URL") or os.getenv("TEST_DATABASE_URL") |
| if not url: |
| raise SystemExit( |
| "DATABASE_URL not set. Export it or pass --database-url. " |
| "Local devs can populate database/.env." |
| ) |
| return url |
|
|
|
|
| def create_tables(url: str, *, include_prod: bool, include_test: bool) -> None: |
| engine = create_engine(url) |
| metadata = MetaData() |
| if include_prod: |
| build_predictions_log_table(PROD_TABLE_NAME, metadata) |
| if include_test: |
| build_predictions_log_table(TEST_TABLE_NAME, metadata) |
| metadata.create_all(engine, checkfirst=True) |
| created = ", ".join(metadata.tables.keys()) |
| logger.info("Tables ensured: %s", created) |
|
|
|
|
| def truncate_test(url: str) -> None: |
| """Wipe the test table. Hard-coded to TEST_TABLE_NAME — production is never touched.""" |
| engine = create_engine(url) |
| with engine.begin() as conn: |
| conn.execute(text(f"TRUNCATE TABLE {TEST_TABLE_NAME} RESTART IDENTITY")) |
| logger.info("Truncated %s", TEST_TABLE_NAME) |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| _load_dotenv_fallback() |
|
|
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--database-url", help="Override env DATABASE_URL") |
| group = parser.add_mutually_exclusive_group(required=True) |
| group.add_argument("--create", action="store_true", help="Create both tables") |
| group.add_argument("--create-test", action="store_true", help="Create test table only") |
| group.add_argument( |
| "--truncate-test", action="store_true", help="Empty the test table" |
| ) |
| args = parser.parse_args(argv) |
|
|
| url = _resolve_database_url(args.database_url) |
| if args.create: |
| create_tables(url, include_prod=True, include_test=True) |
| elif args.create_test: |
| create_tables(url, include_prod=False, include_test=True) |
| elif args.truncate_test: |
| truncate_test(url) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|