fix(db): prefer postgres config in production refs NOISSUE
This commit is contained in:
11
README.md
11
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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user