From 59a7e9787ed596991cfdc7066d7d2e763a144620 Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Fri, 10 Apr 2026 20:37:31 +0200 Subject: [PATCH] fix(db): prefer postgres config in production refs NOISSUE --- README.md | 11 +++++++++ ai_software_factory/.env.example | 2 ++ ai_software_factory/config.py | 34 ++++++++++++++++++++++++++-- ai_software_factory/database.py | 38 +++++++++++++++++++++++++------- ai_software_factory/main.py | 16 ++++++++++++-- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e8fa77a..82ce09c 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,15 @@ GITEA_OWNER=ai-software-factory # Optional legacy fixed-repository mode. Leave empty to create one repo per project. GITEA_REPO= +# Database +# In production, provide PostgreSQL settings. They take precedence over the SQLite default. +# Setting USE_SQLITE=false is still supported if you want to make the choice explicit. +POSTGRES_HOST=postgres.yourserver.com +POSTGRES_PORT=5432 +POSTGRES_USER=ai_software_factory +POSTGRES_PASSWORD=change-me +POSTGRES_DB=ai_software_factory + # n8n N8N_WEBHOOK_URL=http://n8n.yourserver.com/webhook/telegram @@ -90,6 +99,8 @@ docker-compose up -d Check your gitea repository for generated PRs +If you deploy the container with PostgreSQL environment variables set, the service now selects PostgreSQL automatically even though SQLite remains the default for local/test usage. + ## API Endpoints | Endpoint | Method | Description | diff --git a/ai_software_factory/.env.example b/ai_software_factory/.env.example index 72e57f1..9310c31 100644 --- a/ai_software_factory/.env.example +++ b/ai_software_factory/.env.example @@ -30,6 +30,8 @@ TELEGRAM_BOT_TOKEN=your_telegram_bot_token TELEGRAM_CHAT_ID=your_chat_id # PostgreSQL +# In production, provide PostgreSQL settings below. They now take precedence over the SQLite default. +# You can also set USE_SQLITE=false explicitly if you want the intent to be obvious. POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_USER=ai_test diff --git a/ai_software_factory/config.py b/ai_software_factory/config.py index 1b8edb9..bb50354 100644 --- a/ai_software_factory/config.py +++ b/ai_software_factory/config.py @@ -66,6 +66,32 @@ class Settings(BaseSettings): DB_POOL_RECYCLE: int = 3600 DB_POOL_TIMEOUT: int = 30 + @property + def postgres_url(self) -> str: + """Get PostgreSQL URL with trimmed whitespace.""" + return (self.POSTGRES_URL or "").strip() + + @property + def postgres_env_configured(self) -> bool: + """Whether PostgreSQL was explicitly configured via environment variables.""" + if self.postgres_url: + return True + postgres_env_keys = ( + "POSTGRES_HOST", + "POSTGRES_PORT", + "POSTGRES_USER", + "POSTGRES_PASSWORD", + "POSTGRES_DB", + ) + return any(bool(os.environ.get(key, "").strip()) for key in postgres_env_keys) + + @property + def use_sqlite(self) -> bool: + """Whether SQLite should be used as the active database backend.""" + if not self.USE_SQLITE: + return False + return not self.postgres_env_configured + @property def pool(self) -> dict: """Get database pool configuration.""" @@ -79,8 +105,10 @@ class Settings(BaseSettings): @property def database_url(self) -> str: """Get database connection URL.""" - if self.USE_SQLITE: + if self.use_sqlite: return f"sqlite:///{self.SQLITE_DB_PATH}" + if self.postgres_url: + return self.postgres_url return ( f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" @@ -89,8 +117,10 @@ class Settings(BaseSettings): @property def test_database_url(self) -> str: """Get test database connection URL.""" - if self.USE_SQLITE: + if self.use_sqlite: return f"sqlite:///{self.SQLITE_DB_PATH}" + if self.postgres_url: + return self.postgres_url return ( f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_TEST_DB}" diff --git a/ai_software_factory/database.py b/ai_software_factory/database.py index 8dcf2be..b408a20 100644 --- a/ai_software_factory/database.py +++ b/ai_software_factory/database.py @@ -2,6 +2,7 @@ from collections.abc import Generator from pathlib import Path +from urllib.parse import urlparse from alembic import command from alembic.config import Config @@ -17,10 +18,31 @@ except ImportError: from models import Base +def get_database_runtime_summary() -> dict[str, str]: + """Return a human-readable summary of the effective database backend.""" + if settings.use_sqlite: + db_path = str(Path(settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db").expanduser().resolve()) + return { + "backend": "sqlite", + "target": db_path, + "database": db_path, + } + + parsed = urlparse(settings.database_url) + database_name = parsed.path.lstrip("/") or "unknown" + host = parsed.hostname or "unknown-host" + port = str(parsed.port or 5432) + return { + "backend": parsed.scheme.split("+", 1)[0] or "postgresql", + "target": f"{host}:{port}/{database_name}", + "database": database_name, + } + + def get_engine() -> Engine: """Create and return SQLAlchemy engine with connection pooling.""" # Use SQLite for tests, PostgreSQL for production - if settings.USE_SQLITE: + if settings.use_sqlite: db_path = settings.SQLITE_DB_PATH or "/tmp/ai_software_factory_test.db" Path(db_path).expanduser().resolve().parent.mkdir(parents=True, exist_ok=True) db_url = f"sqlite:///{db_path}" @@ -31,7 +53,7 @@ def get_engine() -> Engine: echo=settings.LOG_LEVEL == "DEBUG" ) else: - db_url = settings.POSTGRES_URL or settings.database_url + db_url = settings.database_url # PostgreSQL-specific configuration engine = create_engine( db_url, @@ -43,7 +65,7 @@ def get_engine() -> Engine: ) # Event listener for connection checkout (PostgreSQL only) - if not settings.USE_SQLITE: + if not settings.use_sqlite: @event.listens_for(engine, "checkout") def receive_checkout(dbapi_connection, connection_record, connection_proxy): """Log connection checkout for audit purposes.""" @@ -100,7 +122,7 @@ def get_alembic_config(database_url: str | None = None) -> Config: alembic_ini = package_root / "alembic.ini" config = Config(str(alembic_ini)) config.set_main_option("script_location", str(package_root / "alembic")) - config.set_main_option("sqlalchemy.url", database_url or (settings.database_url if not settings.USE_SQLITE else f"sqlite:///{settings.SQLITE_DB_PATH or '/tmp/ai_software_factory_test.db'}")) + config.set_main_option("sqlalchemy.url", database_url or settings.database_url) return config @@ -116,7 +138,7 @@ def run_migrations(database_url: str | None = None) -> dict: def init_db() -> dict: """Initialize database tables and database if needed.""" - if settings.USE_SQLITE: + if settings.use_sqlite: result = run_migrations() if result["status"] == "success": print("SQLite database migrations applied successfully.") @@ -131,7 +153,7 @@ def init_db() -> dict: return {'status': 'error', 'message': f'Error: {str(e)}'} else: # PostgreSQL - db_url = settings.POSTGRES_URL or settings.database_url + db_url = settings.database_url db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory' try: @@ -180,7 +202,7 @@ def init_db() -> dict: def drop_db() -> dict: """Drop all database tables (use with caution!).""" - if settings.USE_SQLITE: + if settings.use_sqlite: engine = get_engine() try: Base.metadata.drop_all(bind=engine) @@ -190,7 +212,7 @@ def drop_db() -> dict: print(f"Error dropping SQLite tables: {str(e)}") return {'status': 'error', 'message': str(e)} else: - db_url = settings.POSTGRES_URL or settings.database_url + db_url = settings.database_url db_name = db_url.split('/')[-1] if '/' in db_url else 'ai_software_factory' try: diff --git a/ai_software_factory/main.py b/ai_software_factory/main.py index 0ad3463..3212f3b 100644 --- a/ai_software_factory/main.py +++ b/ai_software_factory/main.py @@ -13,6 +13,7 @@ The NiceGUI frontend provides: from __future__ import annotations +from contextlib import asynccontextmanager import json import re from pathlib import Path @@ -42,7 +43,18 @@ except ImportError: __version__ = "0.0.1" -app = FastAPI() + +@asynccontextmanager +async def lifespan(_app: FastAPI): + """Log resolved runtime configuration when the app starts.""" + runtime = database_module.get_database_runtime_summary() + print( + f"Runtime configuration: database_backend={runtime['backend']} target={runtime['target']}" + ) + yield + + +app = FastAPI(lifespan=lifespan) DbSession = Annotated[Session, Depends(database_module.get_db)] PROJECT_ID_PATTERN = re.compile(r"[^a-z0-9]+") @@ -180,7 +192,7 @@ def health_check(): """Health check endpoint.""" return { 'status': 'healthy', - 'database': 'sqlite' if database_module.settings.USE_SQLITE else 'postgresql', + 'database': 'sqlite' if database_module.settings.use_sqlite else 'postgresql', }