kirche-einfach/backend/app/models/flow.py
Jonas Miederer e178ae406d feat: flow engine data layer with tests
- SQLModel tables: flow_definitions/versions/runs, approval_tasks
- Alembic setup with autogenerate (sqlmodel types fixed via script.py.mako)
- Settings extended (shared root .env), db engine/session helpers
- pytest suite: conftest with isolated test DB, 5 model tests green
2026-09-06 22:28:53 +00:00

90 lines
3.7 KiB
Python

"""Flow engine domain models (SQLModel tables)."""
from datetime import datetime, timezone
from uuid import UUID, uuid4
from sqlalchemy import JSON, Column, DateTime, UniqueConstraint
from sqlmodel import Field, SQLModel
def utcnow() -> datetime:
return datetime.now(timezone.utc)
def _ts_column() -> Column:
return Column(DateTime(timezone=True))
def _json_column() -> Column:
# Generic JSON: JSONB on Postgres, JSON/TEXT elsewhere.
return Column(JSON)
class FlowDefinition(SQLModel, table=True):
"""A named decision flow (e.g. 'Bauvorhaben pruefen'). Versions live separately."""
__tablename__ = "flow_definitions"
id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str = Field(index=True, unique=True, max_length=200)
description: str = Field(default="", max_length=2000)
status: str = Field(default="active", max_length=20) # active | archived
# Flow to run after a human approval on this flow's runs (nullable).
continuation_flow_id: UUID | None = Field(
default=None, foreign_key="flow_definitions.id"
)
created_at: datetime = Field(default_factory=utcnow, sa_column=_ts_column())
class FlowVersion(SQLModel, table=True):
"""An immutable version of a flow: the exact Langflow JSON that runs."""
__tablename__ = "flow_versions"
__table_args__ = (UniqueConstraint("flow_id", "version_no"),)
id: UUID = Field(default_factory=uuid4, primary_key=True)
flow_id: UUID = Field(foreign_key="flow_definitions.id", index=True)
version_no: int = Field(index=True)
langflow_json: dict = Field(default_factory=dict, sa_column=_json_column())
# Flow id under which LFX serves this version (set on deploy).
lfx_flow_id: str | None = Field(default=None, max_length=200)
deployed_at: datetime | None = Field(default=None, sa_column=_ts_column())
author: str = Field(default="", max_length=200)
created_at: datetime = Field(default_factory=utcnow, sa_column=_ts_column())
class FlowRun(SQLModel, table=True):
"""One execution of a flow version."""
__tablename__ = "flow_runs"
id: UUID = Field(default_factory=uuid4, primary_key=True)
version_id: UUID = Field(foreign_key="flow_versions.id", index=True)
tenant_id: str = Field(default="default", index=True, max_length=200)
# running | completed | pending_approval | failed
status: str = Field(default="running", index=True, max_length=30)
input: dict | None = Field(default=None, sa_column=_json_column())
output: dict | None = Field(default=None, sa_column=_json_column())
session_id: str | None = Field(default=None, max_length=200)
created_at: datetime = Field(default_factory=utcnow, sa_column=_ts_column())
finished_at: datetime | None = Field(default=None, sa_column=_ts_column())
class ApprovalTask(SQLModel, table=True):
"""Human task created by a flow that reached a terminal approval state."""
__tablename__ = "approval_tasks"
id: UUID = Field(default_factory=uuid4, primary_key=True)
run_id: UUID = Field(foreign_key="flow_runs.id", index=True)
version_id: UUID = Field(foreign_key="flow_versions.id", index=True)
tenant_id: str = Field(default="default", index=True, max_length=200)
title: str = Field(default="", max_length=500)
payload: dict = Field(default_factory=dict, sa_column=_json_column())
continuation_flow_id: UUID | None = Field(default=None)
approver: str | None = Field(default=None, max_length=200)
# open | approved | rejected
status: str = Field(default="open", index=True, max_length=20)
decision_note: str = Field(default="", max_length=2000)
created_at: datetime = Field(default_factory=utcnow, sa_column=_ts_column())
decided_at: datetime | None = Field(default=None, sa_column=_ts_column())