61 lines
1.7 KiB
Python
61 lines
1.7 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
|
|
class WorkflowSpecError(ValueError):
|
|
"""Raised when a workflow spec cannot be parsed or is incomplete."""
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class WorkflowSpec:
|
|
name: str
|
|
provider: str
|
|
frontmatter: dict[str, Any]
|
|
body: str
|
|
source_path: Path
|
|
|
|
|
|
def _split_frontmatter(raw_text: str) -> tuple[str, str]:
|
|
if not raw_text.startswith("---"):
|
|
raise WorkflowSpecError("workflow spec must start with frontmatter")
|
|
|
|
parts = raw_text.split("\n---", 1)
|
|
if len(parts) != 2:
|
|
raise WorkflowSpecError("workflow spec frontmatter is not terminated")
|
|
|
|
frontmatter_text = parts[0][4:]
|
|
body = parts[1].lstrip("\r\n")
|
|
return frontmatter_text, body
|
|
|
|
|
|
def load_workflow_spec(path: str | Path) -> WorkflowSpec:
|
|
source_path = Path(path)
|
|
raw_text = source_path.read_text(encoding="utf-8")
|
|
frontmatter_text, body = _split_frontmatter(raw_text)
|
|
|
|
payload = yaml.safe_load(frontmatter_text) or {}
|
|
if not isinstance(payload, dict):
|
|
raise WorkflowSpecError("workflow spec frontmatter must be a mapping")
|
|
if True in payload and "on" not in payload:
|
|
payload["on"] = payload.pop(True)
|
|
|
|
name = str(payload.get("name") or "").strip()
|
|
provider = str(payload.get("provider") or "").strip()
|
|
if not name:
|
|
raise WorkflowSpecError("workflow spec is missing required field: name")
|
|
if not provider:
|
|
raise WorkflowSpecError("workflow spec is missing required field: provider")
|
|
|
|
return WorkflowSpec(
|
|
name=name,
|
|
provider=provider,
|
|
frontmatter=payload,
|
|
body=body,
|
|
source_path=source_path,
|
|
)
|