chore: switch to all-SQLite stack (drop Postgres service)

This commit is contained in:
Jonas Miederer 2026-09-07 08:39:06 +00:00
parent 74831483eb
commit 7d5ade6b6a
12 changed files with 104 additions and 62 deletions

View file

@ -1,6 +1,7 @@
"""Database engine, sessions, and FastAPI dependency."""
from collections.abc import Iterator
from pathlib import Path
from sqlalchemy import Engine
from sqlmodel import Session, SQLModel, create_engine
@ -10,11 +11,23 @@ from .settings import settings
_engine: Engine | None = None
def _engine_kwargs(url: str) -> dict:
if url.startswith("sqlite"):
# FastAPI runs sync endpoints in a threadpool; connections are shared
# across threads, so defer the same-thread check. File-based SQLite
# needs an existing parent dir (SQLite won't create it).
return {"connect_args": {"check_same_thread": False}}
return {}
def get_engine(url: str | None = None) -> Engine:
"""Process-wide engine singleton (override URL in tests via settings)."""
global _engine
if _engine is None:
_engine = create_engine(url or settings.database_url)
url = url or settings.database_url
if url.startswith("sqlite:///") and not url.startswith("sqlite:///:memory:"):
Path(url.removeprefix("sqlite:///")).parent.mkdir(parents=True, exist_ok=True)
_engine = create_engine(url, **_engine_kwargs(url))
return _engine

View file

@ -13,10 +13,13 @@ class Settings(BaseSettings):
version: str = "0.1.0"
debug: bool = False
database_url: str = "postgresql+psycopg://kirche:kirche@localhost:5432/kirche_einfach"
test_database_url: str = (
"postgresql+psycopg://kirche:kirche@localhost:5432/kirche_einfach_test"
)
# App database (SQLite file). Override via DATABASE_URL for other engines.
database_url: str = str(REPO_ROOT / "backend" / "data" / "kirche.db")
# Canonical flow definitions (source of truth): one Langflow export per file.
flows_dir: str = str(REPO_ROOT / "backend" / "flows")
# Auto-reconcile backend/flows into the DB + LFX while the API is running.
flows_watch_enabled: bool = True
langflow_url: str = "http://localhost:7860"
langflow_api_key: str = ""

View file

@ -7,10 +7,10 @@ dependencies = [
"alembic>=1.14",
"fastapi[standard]>=0.115.0",
"httpx>=0.28",
"psycopg[binary]>=3.2",
"pydantic-settings>=2.0",
"sqlmodel>=0.0.24",
"uvicorn[standard]>=0.30.0",
"watchfiles>=0.24",
]
[build-system]
@ -27,7 +27,7 @@ testpaths = ["tests"]
[dependency-groups]
dev = [
"lfx==1.12.0",
"lfx",
"pytest>=8",
"pytest-asyncio>=0.24",
"pytest-cov>=6",

View file

@ -1,7 +1,8 @@
"""Manual end-to-end smoke: real LFX + real API, frontend-shaped calls.
Usage (from backend/):
TEST_DATABASE_URL=postgresql+psycopg://... uv run python scripts/smoke_e2e.py
uv run python scripts/smoke_e2e.py
Runs the API against a throwaway SQLite file and an ephemeral LFX pass.
Exits non-zero on any unexpected status/body.
"""
@ -11,7 +12,7 @@ import json
import os
import subprocess
import sys
import time
import tempfile
import urllib.request
from pathlib import Path
@ -54,17 +55,31 @@ def check(name: str, cond: bool, detail: str = "") -> None:
def main() -> None:
tmp = Path(tempfile.mkdtemp(prefix="kirche-smoke-"))
flow_dir = Path("/tmp/smoke-e2e-flows")
flow_dir.mkdir(exist_ok=True)
(flow_dir / "seed.json").write_text(
(FIXTURES / "simple_rule.json").read_text()
)
# Migrate a throwaway SQLite schema to head before starting the API.
from alembic import command as alembic_command
from alembic.config import Config
db_url = f"sqlite:///{tmp / 'smoke.db'}"
cfg = Config()
cfg.set_main_option("script_location", str(BACKEND / "migrations"))
cfg.set_main_option("sqlalchemy.url", db_url)
alembic_command.upgrade(cfg, "head")
env = {
**os.environ,
# lfx prefers LANGFLOW_API_KEY over LFX_API_KEY; the repo .env may set
# it. Clear it so the serve key below is the one we send.
"LANGFLOW_API_KEY": "",
"LFX_API_KEY": LFX_KEY,
"DATABASE_URL": os.environ["TEST_DATABASE_URL"],
"DATABASE_URL": db_url,
"FLOWS_WATCH_ENABLED": "false",
"LFX_URL": f"http://127.0.0.1:{LFX_PORT}",
"LFX_API_KEY": LFX_KEY,
"CALLBACK_BASE_URL": f"http://127.0.0.1:{API_PORT}",
}
lfx = subprocess.Popen(

View file

@ -1,25 +1,22 @@
"""Shared pytest fixtures.
Requires TEST_DATABASE_URL (a dedicated Postgres database, e.g. the
`kirche_einfach_test` database from the compose stack). The URL is mapped
onto DATABASE_URL *before* any app module is imported, so settings and the
alembic env both target the test database. Never point this at production.
Tests run on a throwaway SQLite file (no external database required).
`DATABASE_URL` and `FLOWS_DIR` are pointed at a session temp dir *before* any
app module is imported, so settings pick them up at import time. The app
schema is migrated to head once per session via Alembic.
"""
import os
import tempfile
from pathlib import Path
import pytest
TEST_DB_URL = os.environ.get("TEST_DATABASE_URL")
if not TEST_DB_URL:
raise RuntimeError(
"TEST_DATABASE_URL is not set. Refusing to run tests without "
"an explicit test database (see .env.example)."
)
TEST_AREA = Path(tempfile.mkdtemp(prefix="kirche-tests-"))
# Must precede all `app.*` imports: Settings is instantiated at import time.
os.environ["DATABASE_URL"] = TEST_DB_URL
os.environ["DATABASE_URL"] = f"sqlite:///{TEST_AREA / 'test.db'}"
os.environ["FLOWS_DIR"] = str(TEST_AREA / "flows")
BACKEND_DIR = Path(__file__).resolve().parent.parent
@ -115,6 +112,10 @@ def lfx_server(tmp_path_factory):
env = {
**os.environ,
# lfx serve refuses to start without a serve key; requests need it too.
# get_api_key() prefers LANGFLOW_API_KEY over LFX_API_KEY, and the repo
# .env may put a real Langflow key in the environment — clear it so the
# test key below is the one the client sends.
"LANGFLOW_API_KEY": "",
"LFX_API_KEY": "test-lfx-key",
}
proc = subprocess.Popen(

View file

@ -16,7 +16,9 @@ def test_flow_definition_defaults(session):
assert d.status == "active"
assert d.continuation_flow_id is None
assert d.created_at is not None
assert d.created_at.tzinfo is not None
# Timestamps are written as UTC. SQLite does not round-trip tzinfo, so
# after a refresh the value is calendar-time UTC without tzinfo.
assert d.created_at.year >= 2026
def test_flow_definition_name_unique(session):