chore: enforce ruff lint and format standards across backend
This commit is contained in:
parent
a8e312a4f8
commit
a6cc585dd0
39 changed files with 459 additions and 559 deletions
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import json
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
import httpx
|
||||
|
|
@ -12,15 +13,14 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|||
from fastapi.responses import StreamingResponse
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.auth.deps import get_optional_user, resolve_tenant
|
||||
from app.db import get_engine, get_session
|
||||
from app.models import User
|
||||
from app.settings import settings
|
||||
from app.auth.deps import get_optional_user, resolve_tenant
|
||||
|
||||
from . import runner, store
|
||||
from . import input_schema as form_schema
|
||||
from . import runner, store
|
||||
from .deploy import import_from_langflow
|
||||
from .sync import lfx_id_for, sync_from_directory
|
||||
from .langflow_client import (
|
||||
LangflowClient,
|
||||
LangflowError,
|
||||
|
|
@ -33,7 +33,6 @@ from .lfx_client import (
|
|||
LfxError,
|
||||
LfxInvalidFlow,
|
||||
LfxNotFound,
|
||||
LfxRunFailed,
|
||||
LfxUnavailable,
|
||||
)
|
||||
from .schemas import (
|
||||
|
|
@ -52,6 +51,7 @@ from .schemas import (
|
|||
RunRequest,
|
||||
SyncReport,
|
||||
)
|
||||
from .sync import lfx_id_for, sync_from_directory
|
||||
|
||||
flows_router = APIRouter(prefix="/api/flows", tags=["flows"])
|
||||
approvals_router = APIRouter(prefix="/api/approvals", tags=["approvals"])
|
||||
|
|
@ -66,9 +66,7 @@ def get_lfx() -> LfxClient:
|
|||
|
||||
|
||||
def get_langflow() -> LangflowClient:
|
||||
return LangflowClient(
|
||||
settings.langflow_url, api_key=settings.langflow_api_key or ""
|
||||
)
|
||||
return LangflowClient(settings.langflow_url, api_key=settings.langflow_api_key or "")
|
||||
|
||||
|
||||
def _read_definition(d) -> FlowDefinitionRead:
|
||||
|
|
@ -129,7 +127,7 @@ def _read_task(t) -> ApprovalRead:
|
|||
|
||||
|
||||
@flows_router.post("", response_model=FlowDefinitionRead, status_code=201)
|
||||
def create_flow(data: FlowDefinitionCreate, session: Session = Depends(get_db)):
|
||||
def create_flow(data: FlowDefinitionCreate, session: Annotated[Session, Depends(get_db)]):
|
||||
try:
|
||||
return _read_definition(store.create_definition(session, data))
|
||||
except store.ConflictError as exc:
|
||||
|
|
@ -139,14 +137,14 @@ def create_flow(data: FlowDefinitionCreate, session: Session = Depends(get_db)):
|
|||
|
||||
|
||||
@flows_router.get("", response_model=list[FlowDefinitionRead])
|
||||
def list_flows(session: Session = Depends(get_db)):
|
||||
def list_flows(session: Annotated[Session, Depends(get_db)]):
|
||||
return [_read_definition(d) for d in store.list_definitions(session)]
|
||||
|
||||
|
||||
@flows_router.post("/sync", response_model=SyncReport)
|
||||
async def sync_flows(
|
||||
session: Session = Depends(get_db),
|
||||
lfx: LfxClient = Depends(get_lfx),
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
lfx: Annotated[LfxClient, Depends(get_lfx)],
|
||||
):
|
||||
"""Reconcile backend/flows/*.json into the DB + LFX (add/update/delete)."""
|
||||
try:
|
||||
|
|
@ -156,18 +154,16 @@ async def sync_flows(
|
|||
|
||||
|
||||
@flows_router.get("/{flow_id}", response_model=FlowDefinitionRead)
|
||||
def get_flow(flow_id: UUID, session: Session = Depends(get_db)):
|
||||
def get_flow(flow_id: UUID, session: Annotated[Session, Depends(get_db)]):
|
||||
try:
|
||||
return _read_definition(store.get_definition(session, flow_id))
|
||||
except store.NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@flows_router.post(
|
||||
"/{flow_id}/versions", response_model=FlowVersionRead, status_code=201
|
||||
)
|
||||
@flows_router.post("/{flow_id}/versions", response_model=FlowVersionRead, status_code=201)
|
||||
def create_version(
|
||||
flow_id: UUID, data: FlowVersionCreate, session: Session = Depends(get_db)
|
||||
flow_id: UUID, data: FlowVersionCreate, session: Annotated[Session, Depends(get_db)]
|
||||
):
|
||||
try:
|
||||
return _read_version(store.create_version(session, flow_id, data))
|
||||
|
|
@ -178,21 +174,19 @@ def create_version(
|
|||
|
||||
|
||||
@flows_router.get("/{flow_id}/versions", response_model=list[FlowVersionRead])
|
||||
def list_versions(flow_id: UUID, session: Session = Depends(get_db)):
|
||||
def list_versions(flow_id: UUID, session: Annotated[Session, Depends(get_db)]):
|
||||
try:
|
||||
return [_read_version(v) for v in store.list_versions(session, flow_id)]
|
||||
except store.NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@flows_router.post(
|
||||
"/{flow_id}/versions/{version_no}/deploy", response_model=FlowVersionRead
|
||||
)
|
||||
@flows_router.post("/{flow_id}/versions/{version_no}/deploy", response_model=FlowVersionRead)
|
||||
async def deploy_version(
|
||||
flow_id: UUID,
|
||||
version_no: int,
|
||||
session: Session = Depends(get_db),
|
||||
lfx: LfxClient = Depends(get_lfx),
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
lfx: Annotated[LfxClient, Depends(get_lfx)],
|
||||
):
|
||||
try:
|
||||
version = store.get_version_by_no(session, flow_id, version_no)
|
||||
|
|
@ -227,10 +221,10 @@ def _resolve_version(session: Session, flow_id: UUID, version_no: int | None):
|
|||
async def run_flow(
|
||||
flow_id: UUID,
|
||||
data: RunRequest,
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
lfx: Annotated[LfxClient, Depends(get_lfx)],
|
||||
user: Annotated[User | None, Depends(get_optional_user)],
|
||||
stream: bool = Query(default=False),
|
||||
session: Session = Depends(get_db),
|
||||
lfx: LfxClient = Depends(get_lfx),
|
||||
user: User | None = Depends(get_optional_user),
|
||||
):
|
||||
# Tenant comes from the session; the body param stays as a deprecated
|
||||
# fallback for unauthenticated callers.
|
||||
|
|
@ -265,9 +259,7 @@ async def run_flow(
|
|||
|
||||
async def _run_stream(session: Session, lfx: LfxClient, version, data: RunRequest):
|
||||
if not version.lfx_flow_id:
|
||||
raise HTTPException(
|
||||
status_code=422, detail=f"version {version.id} is not deployed to LFX"
|
||||
)
|
||||
raise HTTPException(status_code=422, detail=f"version {version.id} is not deployed to LFX")
|
||||
run = store.create_run(
|
||||
session,
|
||||
version,
|
||||
|
|
@ -288,30 +280,28 @@ async def _run_stream(session: Session, lfx: LfxClient, version, data: RunReques
|
|||
async def event_gen() -> AsyncIterator[str]:
|
||||
yield f"event: run\ndata: {json.dumps({'run_id': str(run_id)})}\n\n"
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
base_url=lfx.base_url, timeout=lfx.timeout, headers=lfx._headers()
|
||||
) as client:
|
||||
async with client.stream(
|
||||
async with (
|
||||
httpx.AsyncClient(
|
||||
base_url=lfx.base_url, timeout=lfx.timeout, headers=lfx._headers()
|
||||
) as client,
|
||||
client.stream(
|
||||
"POST",
|
||||
f"/flows/{lfx_flow_id}/stream",
|
||||
json={"input_value": envelope, "session_id": session_id},
|
||||
) as response:
|
||||
if response.status_code == 404:
|
||||
raise LfxNotFound(f"flow {lfx_flow_id} unknown to LFX")
|
||||
response.raise_for_status()
|
||||
async for chunk in response.aiter_text():
|
||||
yield chunk
|
||||
) as response,
|
||||
):
|
||||
if response.status_code == 404:
|
||||
raise LfxNotFound(f"flow {lfx_flow_id} unknown to LFX")
|
||||
response.raise_for_status()
|
||||
async for chunk in response.aiter_text():
|
||||
yield chunk
|
||||
except (LfxNotFound, httpx.HTTPError) as exc:
|
||||
yield f"event: error\ndata: {json.dumps({'error': str(exc)})}\n\n"
|
||||
# Finalize from a fresh session: the request session may be closed.
|
||||
with Session(get_engine()) as fresh:
|
||||
try:
|
||||
fresh_run = store.get_run(fresh, run_id)
|
||||
status = (
|
||||
"pending_approval"
|
||||
if runner._task_exists(fresh, run_id)
|
||||
else "completed"
|
||||
)
|
||||
status = "pending_approval" if runner._task_exists(fresh, run_id) else "completed"
|
||||
store.set_run_result(fresh, fresh_run, status, {"streamed": True})
|
||||
except store.NotFoundError:
|
||||
status = "failed"
|
||||
|
|
@ -323,9 +313,9 @@ async def _run_stream(session: Session, lfx: LfxClient, version, data: RunReques
|
|||
@flows_router.get("/{flow_id}/runs", response_model=list[RunRead])
|
||||
def list_runs(
|
||||
flow_id: UUID,
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
user: Annotated[User | None, Depends(get_optional_user)],
|
||||
tenant_id: str | None = Query(default=None),
|
||||
session: Session = Depends(get_db),
|
||||
user: User | None = Depends(get_optional_user),
|
||||
):
|
||||
# Authenticated callers are scoped to their session tenant unless they
|
||||
# pass an explicit tenant (deprecated fallback for unauthenticated use).
|
||||
|
|
@ -338,7 +328,7 @@ def list_runs(
|
|||
|
||||
|
||||
@flows_router.get("/langflow/available")
|
||||
async def list_langflow_flows(langflow: LangflowClient = Depends(get_langflow)):
|
||||
async def list_langflow_flows(langflow: Annotated[LangflowClient, Depends(get_langflow)]):
|
||||
"""Browse flows on the Langflow authoring server (admin import source)."""
|
||||
try:
|
||||
return await langflow.list_flows()
|
||||
|
|
@ -351,9 +341,9 @@ async def list_langflow_flows(langflow: LangflowClient = Depends(get_langflow)):
|
|||
@flows_router.post("/import", response_model=FlowImportRead, status_code=201)
|
||||
async def import_flow(
|
||||
data: FlowImportRequest,
|
||||
session: Session = Depends(get_db),
|
||||
langflow: LangflowClient = Depends(get_langflow),
|
||||
lfx: LfxClient = Depends(get_lfx),
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
langflow: Annotated[LangflowClient, Depends(get_langflow)],
|
||||
lfx: Annotated[LfxClient, Depends(get_lfx)],
|
||||
):
|
||||
"""Import a flow from Langflow: export -> version -> (deploy to LFX)."""
|
||||
try:
|
||||
|
|
@ -384,24 +374,20 @@ async def import_flow(
|
|||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
except LfxError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return FlowImportRead(
|
||||
definition=_read_definition(definition), version=_read_version(version)
|
||||
)
|
||||
return FlowImportRead(definition=_read_definition(definition), version=_read_version(version))
|
||||
|
||||
|
||||
@flows_router.get("/{flow_id}/input-schema", response_model=InputSchemaRead)
|
||||
def get_input_schema(
|
||||
flow_id: UUID,
|
||||
session: Session = Depends(get_db),
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
):
|
||||
"""Form schema of the latest deployed version (404 when none)."""
|
||||
try:
|
||||
version = store.get_latest_deployed_version(session, flow_id)
|
||||
except (store.NotFoundError, store.InvalidError) as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
cached = version.input_schema or form_schema.extract_form_schema(
|
||||
version.langflow_json
|
||||
)
|
||||
cached = version.input_schema or form_schema.extract_form_schema(version.langflow_json)
|
||||
return InputSchemaRead(
|
||||
flow_id=flow_id,
|
||||
version_no=version.version_no,
|
||||
|
|
@ -414,9 +400,9 @@ def get_input_schema(
|
|||
@flows_router.get("/runs/{run_id}", response_model=RunRead)
|
||||
def get_run(
|
||||
run_id: UUID,
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
user: Annotated[User | None, Depends(get_optional_user)],
|
||||
tenant_id: str | None = Query(default=None),
|
||||
session: Session = Depends(get_db),
|
||||
user: User | None = Depends(get_optional_user),
|
||||
):
|
||||
"""Read a run. With tenant_id, cross-tenant reads 404 (v1 scoping)."""
|
||||
if user is not None and tenant_id is None:
|
||||
|
|
@ -431,7 +417,7 @@ def get_run(
|
|||
|
||||
|
||||
@approvals_router.post("", response_model=ApprovalRead, status_code=201)
|
||||
def create_approval(data: ApprovalCreate, session: Session = Depends(get_db)):
|
||||
def create_approval(data: ApprovalCreate, session: Annotated[Session, Depends(get_db)]):
|
||||
"""Flow callback contract: report a terminal approval state for a run."""
|
||||
try:
|
||||
return _read_task(store.create_approval_task(session, data))
|
||||
|
|
@ -445,9 +431,9 @@ def create_approval(data: ApprovalCreate, session: Session = Depends(get_db)):
|
|||
|
||||
@approvals_router.get("", response_model=list[ApprovalRead])
|
||||
def list_approvals(
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
user: Annotated[User | None, Depends(get_optional_user)],
|
||||
tenant_id: str | None = Query(default=None),
|
||||
session: Session = Depends(get_db),
|
||||
user: User | None = Depends(get_optional_user),
|
||||
):
|
||||
if user is not None and tenant_id is None:
|
||||
tenant_id = user.tenant_id
|
||||
|
|
@ -458,8 +444,8 @@ def list_approvals(
|
|||
async def decide_approval(
|
||||
task_id: UUID,
|
||||
data: DecisionRequest,
|
||||
session: Session = Depends(get_db),
|
||||
lfx: LfxClient = Depends(get_lfx),
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
lfx: Annotated[LfxClient, Depends(get_lfx)],
|
||||
):
|
||||
"""Decide an open task. Approve starts the continuation flow (if any)."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from sqlmodel import Session
|
|||
from app.models import FlowDefinition, FlowVersion
|
||||
from app.settings import settings
|
||||
|
||||
from . import export, store, sync
|
||||
from . import export, sync
|
||||
from .langflow_client import LangflowClient
|
||||
from .lfx_client import LfxClient
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ def slugify(name: str) -> str:
|
|||
|
||||
def canonical_bytes(doc: dict) -> bytes:
|
||||
"""Deterministic serialization: stable bytes for stable git diffs."""
|
||||
return (json.dumps(doc, indent=2, ensure_ascii=False, sort_keys=True) + "\n").encode(
|
||||
"utf-8"
|
||||
)
|
||||
return (json.dumps(doc, indent=2, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def content_hash(doc: dict) -> str:
|
||||
|
|
@ -38,9 +36,7 @@ def flow_path(flows_dir: str | Path, stem: str) -> Path:
|
|||
return Path(flows_dir) / f"{stem}.json"
|
||||
|
||||
|
||||
def write_flow_file(
|
||||
flows_dir: str | Path, doc: dict, *, name: str | None = None
|
||||
) -> Path:
|
||||
def write_flow_file(flows_dir: str | Path, doc: dict, *, name: str | None = None) -> Path:
|
||||
"""Write a flow document to backend/flows atomically (tmp + rename).
|
||||
|
||||
The filename derives from `name` (or the document's own name). Returns
|
||||
|
|
@ -50,9 +46,7 @@ def write_flow_file(
|
|||
flows_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = slugify(name or doc.get("name") or "flow")
|
||||
target = flow_path(flows_dir, stem)
|
||||
fd, tmp_name = tempfile.mkstemp(
|
||||
dir=str(flows_dir), prefix=f".{stem}-", suffix=".tmp"
|
||||
)
|
||||
fd, tmp_name = tempfile.mkstemp(dir=str(flows_dir), prefix=f".{stem}-", suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as tmp:
|
||||
tmp.write(canonical_bytes(doc))
|
||||
|
|
|
|||
|
|
@ -51,9 +51,7 @@ class LfxRunResult:
|
|||
class LfxClient:
|
||||
"""Thin async wrapper around the LFX serve HTTP API."""
|
||||
|
||||
def __init__(
|
||||
self, base_url: str, timeout: float = 120.0, api_key: str | None = None
|
||||
) -> None:
|
||||
def __init__(self, base_url: str, timeout: float = 120.0, api_key: str | None = None) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout
|
||||
self.api_key = api_key
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ def build_envelope(
|
|||
"payload": payload,
|
||||
"run_id": str(run_id),
|
||||
"callback_url": callback_base_url.rstrip("/"),
|
||||
"continuation_flow_id": (
|
||||
str(continuation_flow_id) if continuation_flow_id else None
|
||||
),
|
||||
"continuation_flow_id": (str(continuation_flow_id) if continuation_flow_id else None),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -36,9 +34,7 @@ def build_envelope(
|
|||
def _task_exists(session: Session, run_id: UUID) -> bool:
|
||||
return (
|
||||
session.exec(
|
||||
select(ApprovalTask).where(
|
||||
ApprovalTask.run_id == run_id, ApprovalTask.status == "open"
|
||||
)
|
||||
select(ApprovalTask).where(ApprovalTask.run_id == run_id, ApprovalTask.status == "open")
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
|
|
@ -80,9 +76,7 @@ async def execute_run(
|
|||
session_id=session_id or str(run.id),
|
||||
)
|
||||
except LfxRunFailed as exc:
|
||||
return store.set_run_result(
|
||||
session, run, "failed", {"error": str(exc)}
|
||||
)
|
||||
return store.set_run_result(session, run, "failed", {"error": str(exc)})
|
||||
output = {
|
||||
"result": result.result,
|
||||
"component": result.component,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
|
@ -27,9 +27,10 @@ class InvalidError(Exception):
|
|||
|
||||
|
||||
def create_definition(session: Session, data: FlowDefinitionCreate) -> FlowDefinition:
|
||||
if data.continuation_flow_id is not None and session.get(
|
||||
FlowDefinition, data.continuation_flow_id
|
||||
) is None:
|
||||
if (
|
||||
data.continuation_flow_id is not None
|
||||
and session.get(FlowDefinition, data.continuation_flow_id) is None
|
||||
):
|
||||
raise InvalidError("continuation_flow_id unknown")
|
||||
definition = FlowDefinition(
|
||||
name=data.name,
|
||||
|
|
@ -57,18 +58,14 @@ def list_definitions(session: Session) -> list[FlowDefinition]:
|
|||
return list(session.exec(select(FlowDefinition).order_by(FlowDefinition.name)).all())
|
||||
|
||||
|
||||
def get_definition_by_source_path(
|
||||
session: Session, source_path: str
|
||||
) -> FlowDefinition | None:
|
||||
def get_definition_by_source_path(session: Session, source_path: str) -> FlowDefinition | None:
|
||||
"""Find the definition synced from a given backend/flows filename."""
|
||||
return session.exec(
|
||||
select(FlowDefinition).where(FlowDefinition.source_path == source_path)
|
||||
).first()
|
||||
|
||||
|
||||
def set_definition_name(
|
||||
session: Session, definition: FlowDefinition, name: str
|
||||
) -> FlowDefinition:
|
||||
def set_definition_name(session: Session, definition: FlowDefinition, name: str) -> FlowDefinition:
|
||||
"""Rename a definition and record its source file. Name clashes 409."""
|
||||
definition.name = name
|
||||
session.add(definition)
|
||||
|
|
@ -88,32 +85,22 @@ def next_version_no(session: Session, flow_id: UUID) -> int:
|
|||
return (max_no or 0) + 1
|
||||
|
||||
|
||||
def delete_definition_cascade(
|
||||
session: Session, definition: FlowDefinition
|
||||
) -> dict[str, int]:
|
||||
def delete_definition_cascade(session: Session, definition: FlowDefinition) -> dict[str, int]:
|
||||
"""Remove a definition and its whole subtree (tasks, runs, versions).
|
||||
|
||||
Used when the canonical file disappears: the directory is the source of
|
||||
truth, so the derived registry follows. Commits once.
|
||||
"""
|
||||
version_ids = list(
|
||||
session.exec(
|
||||
select(FlowVersion.id).where(FlowVersion.flow_id == definition.id)
|
||||
).all()
|
||||
session.exec(select(FlowVersion.id).where(FlowVersion.flow_id == definition.id)).all()
|
||||
)
|
||||
task_count = 0
|
||||
run_count = 0
|
||||
if version_ids:
|
||||
tasks = list(
|
||||
session.exec(
|
||||
select(ApprovalTask).where(ApprovalTask.version_id.in_(version_ids))
|
||||
).all()
|
||||
)
|
||||
runs = list(
|
||||
session.exec(
|
||||
select(FlowRun).where(FlowRun.version_id.in_(version_ids))
|
||||
).all()
|
||||
session.exec(select(ApprovalTask).where(ApprovalTask.version_id.in_(version_ids))).all()
|
||||
)
|
||||
runs = list(session.exec(select(FlowRun).where(FlowRun.version_id.in_(version_ids))).all())
|
||||
task_count, run_count = len(tasks), len(runs)
|
||||
for row in (*tasks, *runs):
|
||||
session.delete(row)
|
||||
|
|
@ -142,9 +129,7 @@ def validate_flow_json(flow_json: dict) -> None:
|
|||
raise InvalidError("langflow_json.data must contain 'nodes' and 'edges' lists")
|
||||
|
||||
|
||||
def create_version(
|
||||
session: Session, flow_id: UUID, data: FlowVersionCreate
|
||||
) -> FlowVersion:
|
||||
def create_version(session: Session, flow_id: UUID, data: FlowVersionCreate) -> FlowVersion:
|
||||
get_definition(session, flow_id)
|
||||
validate_flow_json(data.langflow_json)
|
||||
max_no = session.exec(
|
||||
|
|
@ -207,7 +192,7 @@ def get_latest_deployed_version(session: Session, flow_id: UUID) -> FlowVersion:
|
|||
|
||||
def mark_deployed(session: Session, version: FlowVersion, lfx_flow_id: str) -> FlowVersion:
|
||||
version.lfx_flow_id = lfx_flow_id
|
||||
version.deployed_at = datetime.now(timezone.utc)
|
||||
version.deployed_at = datetime.now(UTC)
|
||||
session.add(version)
|
||||
session.commit()
|
||||
session.refresh(version)
|
||||
|
|
@ -234,12 +219,10 @@ def create_run(
|
|||
return run
|
||||
|
||||
|
||||
def set_run_result(
|
||||
session: Session, run: FlowRun, status: str, output: dict | None
|
||||
) -> FlowRun:
|
||||
def set_run_result(session: Session, run: FlowRun, status: str, output: dict | None) -> FlowRun:
|
||||
run.status = status
|
||||
run.output = output
|
||||
run.finished_at = datetime.now(timezone.utc)
|
||||
run.finished_at = datetime.now(UTC)
|
||||
session.add(run)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
|
|
@ -253,9 +236,7 @@ def get_run(session: Session, run_id: UUID) -> FlowRun:
|
|||
return run
|
||||
|
||||
|
||||
def list_runs(
|
||||
session: Session, flow_id: UUID, tenant_id: str | None = None
|
||||
) -> list[FlowRun]:
|
||||
def list_runs(session: Session, flow_id: UUID, tenant_id: str | None = None) -> list[FlowRun]:
|
||||
get_definition(session, flow_id)
|
||||
query = (
|
||||
select(FlowRun)
|
||||
|
|
@ -272,9 +253,10 @@ def create_approval_task(session: Session, data: ApprovalCreate) -> ApprovalTask
|
|||
run = session.get(FlowRun, data.run_id)
|
||||
if run is None:
|
||||
raise NotFoundError(f"run {data.run_id} not found")
|
||||
if data.continuation_flow_id is not None and session.get(
|
||||
FlowDefinition, data.continuation_flow_id
|
||||
) is None:
|
||||
if (
|
||||
data.continuation_flow_id is not None
|
||||
and session.get(FlowDefinition, data.continuation_flow_id) is None
|
||||
):
|
||||
raise InvalidError("continuation_flow_id unknown")
|
||||
existing = session.exec(
|
||||
select(ApprovalTask).where(
|
||||
|
|
@ -293,7 +275,7 @@ def create_approval_task(session: Session, data: ApprovalCreate) -> ApprovalTask
|
|||
)
|
||||
session.add(task)
|
||||
run.status = "pending_approval"
|
||||
run.finished_at = datetime.now(timezone.utc)
|
||||
run.finished_at = datetime.now(UTC)
|
||||
session.add(run)
|
||||
session.commit()
|
||||
session.refresh(task)
|
||||
|
|
@ -330,7 +312,7 @@ def decide_task(
|
|||
task.status = decision
|
||||
task.approver = approver
|
||||
task.decision_note = note
|
||||
task.decided_at = datetime.now(timezone.utc)
|
||||
task.decided_at = datetime.now(UTC)
|
||||
session.add(task)
|
||||
session.commit()
|
||||
session.refresh(task)
|
||||
|
|
|
|||
|
|
@ -41,9 +41,7 @@ def lfx_id_for(definition_id: UUID, version_no: int) -> str:
|
|||
Same (definition, version) always maps to the same id, so re-syncs and
|
||||
crash recovery overwrite instead of orphaning LFX registrations.
|
||||
"""
|
||||
return str(
|
||||
uuid.uuid5(uuid.NAMESPACE_URL, f"kirche-einfach/{definition_id}/v{version_no}")
|
||||
)
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"kirche-einfach/{definition_id}/v{version_no}"))
|
||||
|
||||
|
||||
def _latest_version(session: Session, flow_id: UUID) -> FlowVersion | None:
|
||||
|
|
@ -72,9 +70,7 @@ async def sync_document(
|
|||
with sync_lock:
|
||||
definition = store.get_definition_by_source_path(session, source_filename)
|
||||
if definition is None:
|
||||
definition = store.create_definition(
|
||||
session, FlowDefinitionCreate(name=name)
|
||||
)
|
||||
definition = store.create_definition(session, FlowDefinitionCreate(name=name))
|
||||
definition.source_path = source_filename
|
||||
session.add(definition)
|
||||
session.commit()
|
||||
|
|
@ -86,10 +82,8 @@ async def sync_document(
|
|||
store.set_definition_name(session, definition, name)
|
||||
|
||||
latest = _latest_version(session, definition.id)
|
||||
if (
|
||||
latest is not None
|
||||
and export.content_hash(latest.langflow_json)
|
||||
== export.content_hash(doc)
|
||||
if latest is not None and export.content_hash(latest.langflow_json) == export.content_hash(
|
||||
doc
|
||||
):
|
||||
return definition, latest, "skipped"
|
||||
|
||||
|
|
@ -110,15 +104,9 @@ async def sync_document(
|
|||
FlowVersionCreate(langflow_json=doc, author=author),
|
||||
)
|
||||
if version.version_no != version_no: # pragma: no cover - lock prevents this
|
||||
raise store.ConflictError(
|
||||
f"concurrent version write for flow {definition.id}"
|
||||
)
|
||||
raise store.ConflictError(f"concurrent version write for flow {definition.id}")
|
||||
version = store.mark_deployed(session, version, uploaded.id)
|
||||
if (
|
||||
latest is not None
|
||||
and latest.lfx_flow_id
|
||||
and latest.lfx_flow_id != uploaded.id
|
||||
):
|
||||
if latest is not None and latest.lfx_flow_id and latest.lfx_flow_id != uploaded.id:
|
||||
try:
|
||||
await lfx.delete_flow(latest.lfx_flow_id)
|
||||
except LfxError as exc:
|
||||
|
|
@ -175,10 +163,8 @@ async def sync_from_directory(
|
|||
report.errors[filename] = "top-level JSON must be an object"
|
||||
continue
|
||||
try:
|
||||
_, _, action = await sync_document(
|
||||
session, lfx, doc, source_filename=filename
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - reconciliation is total
|
||||
_, _, action = await sync_document(session, lfx, doc, source_filename=filename)
|
||||
except (store.InvalidError, store.ConflictError, store.NotFoundError, LfxError) as exc:
|
||||
report.errors[filename] = str(exc)
|
||||
continue
|
||||
getattr(report, action).append(path.stem)
|
||||
|
|
@ -193,7 +179,7 @@ async def sync_from_directory(
|
|||
continue
|
||||
try:
|
||||
await delete_definition_tree(session, lfx, definition)
|
||||
except Exception as exc: # noqa: BLE001 - reconciliation is total
|
||||
except store.NotFoundError as exc:
|
||||
report.errors[stale] = str(exc)
|
||||
continue
|
||||
report.deleted.append(Path(stale).stem)
|
||||
|
|
@ -205,9 +191,7 @@ def default_session_factory() -> Callable[[], Session]:
|
|||
|
||||
|
||||
def default_lfx_factory() -> Callable[[], LfxClient]:
|
||||
return lambda: LfxClient(
|
||||
settings.lfx_url, api_key=settings.lfx_api_key or None
|
||||
)
|
||||
return lambda: LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
|
||||
|
||||
|
||||
def run_sync_blocking(
|
||||
|
|
@ -246,14 +230,12 @@ async def watch_forever(
|
|||
logger.error("flows dir missing, retrying in 30s: %s", flows_dir)
|
||||
try:
|
||||
await asyncio.wait_for(stop.wait(), timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
pass
|
||||
if stop.is_set():
|
||||
return
|
||||
logger.info("watching %s", flows_dir)
|
||||
async for _ in awatch(
|
||||
flows_dir, watch_filter=_JsonFilter(), debounce=1000, step=100
|
||||
):
|
||||
async for _ in awatch(flows_dir, watch_filter=_JsonFilter(), debounce=1000, step=100):
|
||||
if stop.is_set():
|
||||
break
|
||||
try:
|
||||
|
|
@ -261,7 +243,7 @@ async def watch_forever(
|
|||
except FileNotFoundError as exc:
|
||||
logger.error("flow sync failed: %s", exc)
|
||||
continue
|
||||
except Exception: # noqa: BLE001 - watcher must survive
|
||||
except Exception:
|
||||
logger.exception("flow sync failed")
|
||||
continue
|
||||
logger.info(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue