diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27f0711..b577da9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,20 +77,19 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - - name: Create .env for tests - run: | - printf "KEYWARDEN_POSTGRES_DSN=%s\nKEYWARDEN_SECRET_KEY=%s\nKEYWARDEN_ACCESS_TOKEN_EXPIRE_MINUTES=60\n" \ - "${{ env.TEST_POSTGRES_DSN }}" "testsecret" > .env - echo "Wrote .env with DSN=${{ env.TEST_POSTGRES_DSN }}" - - name: Set PYTHONPATH run: echo "PYTHONPATH=${GITHUB_WORKSPACE}" >> $GITHUB_ENV + - name: Create .env for tests (optional for app runtime) + run: | + printf "KEYWARDEN_POSTGRES_DSN=%s\nKEYWARDEN_SECRET_KEY=%s\n" \ + "${{ env.TEST_POSTGRES_DSN }}" "testsecret" > .env + - name: Run Alembic migrations env: KEYWARDEN_POSTGRES_DSN: ${{ env.TEST_POSTGRES_DSN }} - run: | - alembic upgrade head + run: alembic upgrade head + - name: Pytest env: KEYWARDEN_POSTGRES_DSN: ${{ env.TEST_POSTGRES_DSN }} diff --git a/alembic/env.py b/alembic/env.py index 19860d1..c94ef15 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,62 +1,57 @@ -import sys, pathlib +import os +import sys +import pathlib +import asyncio +from logging.config import fileConfig -# Add project root (parent of the "alembic" dir) to sys.path +from alembic import context +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine + +# Ensure project root is importable PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from logging.config import fileConfig -from alembic import context +# Import metadata (should NOT import settings) +from app.db.base import Base # Base.metadata must include all models -# Import your app's config & models -from app.core.config import settings -from app.db.base import Base # imports all models via app/models/__init__.py - -# Alembic Config object +# Alembic config & logging config = context.config - -# Set up Python logging from alembic.ini if config.config_file_name is not None: fileConfig(config.config_file_name) -# Target metadata (for autogenerate) target_metadata = Base.metadata -# DSN from your app config (e.g. postgresql+asyncpg://…) -DB_URL = settings.POSTGRES_DSN +# Get DB URL from env (prefer KEYWARDEN_ prefix, fall back to unprefixed, then a sane default for local) +DB_URL = ( + os.getenv("KEYWARDEN_POSTGRES_DSN") + or os.getenv("POSTGRES_DSN") + or "postgresql+asyncpg://postgres:postgres@localhost:5432/keywarden" +) def run_migrations_offline(): - """Run migrations in 'offline' mode.""" - url = DB_URL context.configure( - url=url, + url=DB_URL, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) - with context.begin_transaction(): context.run_migrations() def do_run_migrations(connection): context.configure(connection=connection, target_metadata=target_metadata) - with context.begin_transaction(): context.run_migrations() async def run_migrations_online(): - """Run migrations in 'online' mode with async engine.""" - connectable: AsyncEngine = create_async_engine( - DB_URL, - poolclass=pool.NullPool, - ) - + connectable: AsyncEngine = create_async_engine(DB_URL, poolclass=pool.NullPool) async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) - await connectable.dispose() diff --git a/app/core/config.py b/app/core/config.py index 1f872ec..9b83088 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,11 +1,16 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + env_prefix="KEYWARDEN_", + extra="ignore", + ) PROJECT_NAME: str = "Keywarden" API_V1_STR: str = "/api/v1" SECRET_KEY: str ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 - POSTGRES_DSN: str # e.g. postgresql+asyncpg://user:pass@db:5432/keywarden + POSTGRES_DSN: str OIDC_ISSUER: str | None = None OIDC_CLIENT_ID: str | None = None OIDC_CLIENT_SECRET: str | None = None