workflow capabilityref: more models
This commit is contained in:
@@ -577,6 +577,11 @@ Dot-joined names remain the wire/presentation format, but new runtime code
|
|||||||
should parse or format through those refs instead of rediscovering source/name
|
should parse or format through those refs instead of rediscovering source/name
|
||||||
boundaries with ad hoc string splits.
|
boundaries with ad hoc string splits.
|
||||||
|
|
||||||
|
Saved workflow artifact names use a separate grammar and ref type:
|
||||||
|
`WorkflowCapabilityRef(artifact_id, version)` serializes as
|
||||||
|
`workflow.<artifact_id>.v<version>`. Artifact ids may contain dots, so this
|
||||||
|
must not be parsed as a generic `CapabilityRef`.
|
||||||
|
|
||||||
The first implementation should prefer artifact validation and dependency
|
The first implementation should prefer artifact validation and dependency
|
||||||
diagnostics before attempting persistent nested resume.
|
diagnostics before attempting persistent nested resume.
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .models import (
|
|||||||
WorkflowArtifact,
|
WorkflowArtifact,
|
||||||
WorkflowDeployment,
|
WorkflowDeployment,
|
||||||
)
|
)
|
||||||
|
from .refs import WorkflowCapabilityRef
|
||||||
from .store import FileWorkflowArtifactStore, WorkflowArtifactStore
|
from .store import FileWorkflowArtifactStore, WorkflowArtifactStore
|
||||||
from .validation import validate_deployment_dependencies
|
from .validation import validate_deployment_dependencies
|
||||||
from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs
|
from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs
|
||||||
@@ -30,6 +31,7 @@ __all__ = [
|
|||||||
"RequiredCapability",
|
"RequiredCapability",
|
||||||
"WorkflowArtifact",
|
"WorkflowArtifact",
|
||||||
"WorkflowArtifactCatalogEntry",
|
"WorkflowArtifactCatalogEntry",
|
||||||
|
"WorkflowCapabilityRef",
|
||||||
"WorkflowArtifactStore",
|
"WorkflowArtifactStore",
|
||||||
"WorkflowDeployment",
|
"WorkflowDeployment",
|
||||||
"artifact_catalog_entry",
|
"artifact_catalog_entry",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from .models import DependencyDiagnostic, JsonObject, WorkflowArtifact
|
from .models import DependencyDiagnostic, JsonObject, WorkflowArtifact
|
||||||
|
from .refs import WorkflowCapabilityRef
|
||||||
|
|
||||||
|
|
||||||
class WorkflowArtifactCatalogEntry(BaseModel):
|
class WorkflowArtifactCatalogEntry(BaseModel):
|
||||||
@@ -23,7 +24,12 @@ class WorkflowArtifactCatalogEntry(BaseModel):
|
|||||||
|
|
||||||
def artifact_node_name(artifact: WorkflowArtifact) -> str:
|
def artifact_node_name(artifact: WorkflowArtifact) -> str:
|
||||||
"""Return the stable planner name for an artifact version."""
|
"""Return the stable planner name for an artifact version."""
|
||||||
return f"workflow.{artifact.id}.v{artifact.version}"
|
return str(
|
||||||
|
WorkflowCapabilityRef(
|
||||||
|
artifact_id=artifact.id,
|
||||||
|
version=artifact.version,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def artifact_catalog_entry(
|
def artifact_catalog_entry(
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WorkflowCapabilityRef:
|
||||||
|
"""Stable public capability name for one saved workflow artifact version."""
|
||||||
|
|
||||||
|
artifact_id: str
|
||||||
|
version: int
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.artifact_id:
|
||||||
|
raise ValueError("workflow capability ref requires an artifact id")
|
||||||
|
if self.version < 1:
|
||||||
|
raise ValueError("workflow capability ref requires version >= 1")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, value: str) -> WorkflowCapabilityRef:
|
||||||
|
"""Parse `workflow.<artifact_id>.v<version>` into first-class fields."""
|
||||||
|
prefix = "workflow."
|
||||||
|
if not value.startswith(prefix):
|
||||||
|
raise ValueError("workflow capability ref must use the workflow namespace")
|
||||||
|
artifact_part, separator, version_part = value[len(prefix) :].rpartition(".v")
|
||||||
|
if not separator or not artifact_part or not version_part.isdecimal():
|
||||||
|
raise ValueError(
|
||||||
|
"workflow capability ref must be workflow.<artifact_id>.v<version>"
|
||||||
|
)
|
||||||
|
return cls(artifact_id=artifact_part, version=int(version_part))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"workflow.{self.artifact_id}.v{self.version}"
|
||||||
@@ -11,6 +11,7 @@ from wf_artifacts import (
|
|||||||
DiagnosticSeverity,
|
DiagnosticSeverity,
|
||||||
RequiredCapability,
|
RequiredCapability,
|
||||||
WorkflowArtifact,
|
WorkflowArtifact,
|
||||||
|
WorkflowCapabilityRef,
|
||||||
WorkflowDeployment,
|
WorkflowDeployment,
|
||||||
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
|
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
|
||||||
validate_deployment_dependencies,
|
validate_deployment_dependencies,
|
||||||
@@ -356,20 +357,21 @@ def _capability_name(qualified_name: str) -> str | None:
|
|||||||
|
|
||||||
def _artifact_capability_id(artifact: WorkflowArtifact) -> str:
|
def _artifact_capability_id(artifact: WorkflowArtifact) -> str:
|
||||||
"""Use the same stable name shape as workflow artifact catalog entries."""
|
"""Use the same stable name shape as workflow artifact catalog entries."""
|
||||||
return f"workflow.{artifact.id}.v{artifact.version}"
|
return str(
|
||||||
|
WorkflowCapabilityRef(
|
||||||
|
artifact_id=artifact.id,
|
||||||
|
version=artifact.version,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_artifact_capability_id(qualified_name: str) -> tuple[str, int] | None:
|
def _parse_artifact_capability_id(qualified_name: str) -> tuple[str, int] | None:
|
||||||
"""Parse the stable `workflow.<artifact_id>.v<version>` capability name."""
|
"""Parse the stable `workflow.<artifact_id>.v<version>` capability name."""
|
||||||
prefix = "workflow."
|
try:
|
||||||
if not qualified_name.startswith(prefix):
|
ref = WorkflowCapabilityRef.parse(qualified_name)
|
||||||
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
artifact_part, separator, version_part = qualified_name[len(prefix) :].rpartition(
|
return ref.artifact_id, ref.version
|
||||||
".v"
|
|
||||||
)
|
|
||||||
if not separator or not artifact_part or not version_part.isdecimal():
|
|
||||||
return None
|
|
||||||
return artifact_part, int(version_part)
|
|
||||||
|
|
||||||
|
|
||||||
def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan:
|
def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan:
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_artifacts import WorkflowCapabilityRef
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_capability_ref_round_trips() -> None:
|
||||||
|
ref = WorkflowCapabilityRef(artifact_id="echo_wrapper", version=2)
|
||||||
|
|
||||||
|
assert str(ref) == "workflow.echo_wrapper.v2"
|
||||||
|
assert WorkflowCapabilityRef.parse(str(ref)) == ref
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_capability_ref_preserves_dotted_artifact_ids() -> None:
|
||||||
|
ref = WorkflowCapabilityRef.parse("workflow.crm.lookup.v3")
|
||||||
|
|
||||||
|
assert ref.artifact_id == "crm.lookup"
|
||||||
|
assert ref.version == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_capability_ref_rejects_other_namespaces() -> None:
|
||||||
|
try:
|
||||||
|
WorkflowCapabilityRef.parse("demo.echo.v1")
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "workflow" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected non-workflow ref to be rejected")
|
||||||
Reference in New Issue
Block a user