gateway/modules/workflows/automation2/runEnvelope.py

109 lines
3.5 KiB
Python

# Copyright (c) 2025 Patrick Motsch
"""
Unified run envelope for Automation2 start/trigger nodes.
Downstream nodes always see the same structure regardless of entry point
(manual, form, schedule, webhook, email, api, event).
"""
from copy import deepcopy
from typing import Any, Dict, List, Optional
# trigger.type values
TRIGGER_TYPES = frozenset(
{
"manual",
"form",
"schedule",
"email",
"webhook",
"api",
"event",
}
)
def default_run_envelope(
trigger_type: str = "manual",
*,
entry_point_id: Optional[str] = None,
entry_point_label: Optional[str] = None,
payload: Optional[Dict[str, Any]] = None,
context: Optional[Dict[str, Any]] = None,
files: Optional[List[Any]] = None,
user: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
raw: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Build a normalized run envelope dict."""
tt = trigger_type if trigger_type in TRIGGER_TYPES else "manual"
trig: Dict[str, Any] = {"type": tt}
if entry_point_id:
trig["entryPointId"] = entry_point_id
if entry_point_label:
trig["label"] = entry_point_label
return {
"trigger": trig,
"payload": dict(payload or {}),
"context": dict(context or {}),
"files": list(files or []),
"user": dict(user or {}),
"metadata": dict(metadata or {}),
"raw": dict(raw or {}),
}
def merge_run_envelope(base: Dict[str, Any], overrides: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Deep-merge overrides into a copy of base (shallow merge per top-level key except nested dicts)."""
out = deepcopy(base)
if not overrides:
return out
for key in ("payload", "context", "user", "metadata", "raw"):
if key in overrides and isinstance(overrides[key], dict):
merged = dict(out.get(key) or {})
merged.update(overrides[key])
out[key] = merged
if "files" in overrides and overrides["files"] is not None:
out["files"] = list(overrides["files"])
trig = dict(out.get("trigger") or {})
ot = overrides.get("trigger")
if isinstance(ot, dict):
trig.update(ot)
if trig.get("type") and trig["type"] not in TRIGGER_TYPES:
trig["type"] = "manual"
out["trigger"] = trig
return out
def normalize_run_envelope(
incoming: Optional[Dict[str, Any]],
*,
user_id: Optional[str] = None,
) -> Dict[str, Any]:
"""
Normalize partial or missing envelope from API/scheduler.
Ensures all top-level keys exist.
"""
if not incoming or not isinstance(incoming, dict):
env = default_run_envelope("manual")
else:
trig = incoming.get("trigger") if isinstance(incoming.get("trigger"), dict) else {}
ttype = trig.get("type") or "manual"
if ttype not in TRIGGER_TYPES:
ttype = "manual"
env = default_run_envelope(
ttype,
entry_point_id=trig.get("entryPointId"),
entry_point_label=trig.get("label"),
payload=incoming.get("payload"),
context=incoming.get("context"),
files=incoming.get("files"),
user=incoming.get("user"),
metadata=incoming.get("metadata"),
raw=incoming.get("raw"),
)
if user_id and not env.get("user"):
env["user"] = {"id": user_id}
elif user_id and isinstance(env.get("user"), dict) and "id" not in env["user"]:
env["user"] = {**env["user"], "id": user_id}
return env