# Copyright (c) 2025 Patrick Motsch """ Workflow entry points (Starts) — configuration outside the flow editor. Kinds align with run envelope trigger.type where applicable. """ import uuid from typing import Any, Dict, List, Optional # On-demand (gear: Manueller Trigger, Formular) KINDS_ON_DEMAND = frozenset({"manual", "form", "api"}) # Always-on (gear: Zeitplan, Immer aktiv, plus legacy listener kinds) KINDS_ALWAYS_ON = frozenset({"schedule", "always_on", "email", "webhook", "event"}) ALL_KINDS = KINDS_ON_DEMAND | KINDS_ALWAYS_ON def category_for_kind(kind: str) -> str: if kind in KINDS_ALWAYS_ON: return "always_on" return "on_demand" def default_manual_entry_point() -> Dict[str, Any]: """Single default manual start when a workflow has no invocations yet.""" return { "id": str(uuid.uuid4()), "kind": "manual", "category": "on_demand", "enabled": True, "title": "Jetzt ausführen", "description": {}, "config": {}, } def _normalize_title(title: Any, preferredLang: Optional[str] = None) -> str: if isinstance(title, dict): picked = (preferredLang and title.get(preferredLang)) or title.get("xx") or next(iter(title.values()), "") return str(picked).strip() if picked is not None else "" if isinstance(title, str) and title.strip(): return title.strip() return "Start" def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a single entry point dict.""" kind = (raw.get("kind") or "manual").strip() if kind not in ALL_KINDS: kind = "manual" cat = raw.get("category") if cat not in ("on_demand", "always_on"): cat = category_for_kind(kind) eid = raw.get("id") or str(uuid.uuid4()) enabled = raw.get("enabled", True) if not isinstance(enabled, bool): enabled = bool(enabled) config = raw.get("config") if isinstance(raw.get("config"), dict) else {} desc = raw.get("description") if isinstance(raw.get("description"), dict) else {} return { "id": str(eid), "kind": kind, "category": cat, "enabled": enabled, "title": _normalize_title(raw.get("title")), "description": desc, "config": config, } def normalize_invocations_list(items: Optional[List[Any]]) -> List[Dict[str, Any]]: if not items: return [default_manual_entry_point()] out: List[Dict[str, Any]] = [] for raw in items: if isinstance(raw, dict): out.append(normalize_invocation_entry(raw)) if not out: return [default_manual_entry_point()] return out # Schedule / cron: wire an external job runner (APScheduler, Celery, system cron) to call # POST .../execute with entryPointId set to a schedule entry — no separate in-process scheduler here yet. def find_invocation(workflow: Dict[str, Any], entry_point_id: str) -> Optional[Dict[str, Any]]: for inv in workflow.get("invocations") or []: if isinstance(inv, dict) and inv.get("id") == entry_point_id: return inv return None