feat: type artifact operation results
This commit is contained in:
@@ -71,9 +71,9 @@
|
|||||||
- A 2026-07-30 spike confirmed that `fastapi-jsonrpc` already exports a
|
- A 2026-07-30 spike confirmed that `fastapi-jsonrpc` already exports a
|
||||||
complete OpenRPC document for all 70 registered methods. Request payloads
|
complete OpenRPC document for all 70 registered methods. Request payloads
|
||||||
retain useful Pydantic schemas, so OpenRPC is a viable transport input.
|
retain useful Pydantic schemas, so OpenRPC is a viable transport input.
|
||||||
- The first typed-result slice now gives `workflow.health` and all deployment
|
- Typed-result slices now give `workflow.health` and all artifact, deployment,
|
||||||
and run operations named transport-neutral result schemas: 11 of 70
|
and run operations named transport-neutral result schemas: 16 of 70
|
||||||
methods. The remaining 59 success results still collapse to generic objects
|
methods. The remaining 54 success results still collapse to generic objects
|
||||||
because their Python API and JSON-RPC handlers return `dict[str, Any]`.
|
because their Python API and JSON-RPC handlers return `dict[str, Any]`.
|
||||||
Continue introducing operation result DTOs before adopting generated
|
Continue introducing operation result DTOs before adopting generated
|
||||||
TypeScript contracts.
|
TypeScript contracts.
|
||||||
|
|||||||
+57
-31
@@ -24,9 +24,21 @@ from .artifact_refs import artifact_capability_id
|
|||||||
from .capability_requirements import observed_node_specs
|
from .capability_requirements import observed_node_specs
|
||||||
from .drafts import WorkflowDraftApi
|
from .drafts import WorkflowDraftApi
|
||||||
from .listing import matches_query, paged_list_payload
|
from .listing import matches_query, paged_list_payload
|
||||||
from .models import RawWorkflowPlan
|
from .models import (
|
||||||
|
DeleteArtifactResult,
|
||||||
|
JsonProjector,
|
||||||
|
ListArtifactsResult,
|
||||||
|
RawWorkflowPlan,
|
||||||
|
SaveArtifactResult,
|
||||||
|
WorkflowArtifactPayload,
|
||||||
|
)
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
|
|
||||||
|
_PROJECT_ARTIFACT = JsonProjector(WorkflowArtifactPayload)
|
||||||
|
_PROJECT_ARTIFACT_LIST = JsonProjector(ListArtifactsResult)
|
||||||
|
_PROJECT_ARTIFACT_SAVE = JsonProjector(SaveArtifactResult)
|
||||||
|
_PROJECT_ARTIFACT_DELETE = JsonProjector(DeleteArtifactResult)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowArtifactApi:
|
class WorkflowArtifactApi:
|
||||||
"""Saved workflow artifact operations.
|
"""Saved workflow artifact operations.
|
||||||
@@ -51,14 +63,16 @@ class WorkflowArtifactApi:
|
|||||||
kind: ArtifactKind | None = None,
|
kind: ArtifactKind | None = None,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> ListArtifactsResult:
|
||||||
"""Return compact paged saved artifact summaries.
|
"""Return compact paged saved artifact summaries.
|
||||||
|
|
||||||
Saved artifacts can contain full raw workflow plans, so list results
|
Saved artifacts can contain full raw workflow plans, so list results
|
||||||
deliberately stay summary-only. Use inspect/run tools for detail.
|
deliberately stay summary-only. Use inspect/run tools for detail.
|
||||||
"""
|
"""
|
||||||
if self.context.artifact_store is None:
|
if self.context.artifact_store is None:
|
||||||
return paged_list_payload("nodes", [], cursor=cursor, limit=limit)
|
return _PROJECT_ARTIFACT_LIST(
|
||||||
|
paged_list_payload("nodes", [], cursor=cursor, limit=limit)
|
||||||
|
)
|
||||||
entries = [
|
entries = [
|
||||||
artifact_catalog_entry(artifact).model_dump(mode="json")
|
artifact_catalog_entry(artifact).model_dump(mode="json")
|
||||||
for artifact in self.context.artifact_store.list_artifacts()
|
for artifact in self.context.artifact_store.list_artifacts()
|
||||||
@@ -77,9 +91,13 @@ class WorkflowArtifactApi:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
entries.sort(key=lambda entry: str(entry.get("name", "")))
|
entries.sort(key=lambda entry: str(entry.get("name", "")))
|
||||||
return paged_list_payload("nodes", entries, cursor=cursor, limit=limit)
|
return _PROJECT_ARTIFACT_LIST(
|
||||||
|
paged_list_payload("nodes", entries, cursor=cursor, limit=limit)
|
||||||
|
)
|
||||||
|
|
||||||
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
|
async def save_artifact(
|
||||||
|
self, artifact: dict[str, Any]
|
||||||
|
) -> SaveArtifactResult:
|
||||||
workflow_artifact = WorkflowArtifact.model_validate(artifact)
|
workflow_artifact = WorkflowArtifact.model_validate(artifact)
|
||||||
self._artifact_store().save_artifact(workflow_artifact)
|
self._artifact_store().save_artifact(workflow_artifact)
|
||||||
self.context.events.record_workflow_event(
|
self.context.events.record_workflow_event(
|
||||||
@@ -90,11 +108,13 @@ class WorkflowArtifactApi:
|
|||||||
"version": workflow_artifact.version,
|
"version": workflow_artifact.version,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return {
|
return _PROJECT_ARTIFACT_SAVE(
|
||||||
"artifact_id": workflow_artifact.id,
|
{
|
||||||
"version": workflow_artifact.version,
|
"artifact_id": workflow_artifact.id,
|
||||||
"saved": True,
|
"version": workflow_artifact.version,
|
||||||
}
|
"saved": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
self,
|
self,
|
||||||
@@ -109,7 +129,7 @@ class WorkflowArtifactApi:
|
|||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
typed_plan = (
|
typed_plan = (
|
||||||
plan
|
plan
|
||||||
if isinstance(plan, RawWorkflowPlan)
|
if isinstance(plan, RawWorkflowPlan)
|
||||||
@@ -141,11 +161,13 @@ class WorkflowArtifactApi:
|
|||||||
"created_from_plan": True,
|
"created_from_plan": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return {
|
return _PROJECT_ARTIFACT_SAVE(
|
||||||
"artifact_id": workflow_artifact.id,
|
{
|
||||||
"version": workflow_artifact.version,
|
"artifact_id": workflow_artifact.id,
|
||||||
"saved": True,
|
"version": workflow_artifact.version,
|
||||||
}
|
"saved": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def create_artifact_from_draft(
|
async def create_artifact_from_draft(
|
||||||
self,
|
self,
|
||||||
@@ -274,35 +296,39 @@ class WorkflowArtifactApi:
|
|||||||
|
|
||||||
async def inspect_artifact(
|
async def inspect_artifact(
|
||||||
self, *, artifact_id: str, version: int
|
self, *, artifact_id: str, version: int
|
||||||
) -> dict[str, Any]:
|
) -> WorkflowArtifactPayload:
|
||||||
artifact = self._artifact_store().get_artifact(artifact_id, version)
|
artifact = self._artifact_store().get_artifact(artifact_id, version)
|
||||||
return artifact.model_dump(mode="json")
|
return _PROJECT_ARTIFACT(artifact.model_dump(mode="json"))
|
||||||
|
|
||||||
async def delete_artifact(
|
async def delete_artifact(
|
||||||
self, *, artifact_id: str, version: int
|
self, *, artifact_id: str, version: int
|
||||||
) -> dict[str, Any]:
|
) -> DeleteArtifactResult:
|
||||||
store = self._artifact_store()
|
store = self._artifact_store()
|
||||||
blockers = store.deployments_for_artifact(artifact_id, version)
|
blockers = store.deployments_for_artifact(artifact_id, version)
|
||||||
blocker_ids = [deployment.id for deployment in blockers]
|
blocker_ids = [deployment.id for deployment in blockers]
|
||||||
if blocker_ids:
|
if blocker_ids:
|
||||||
return {
|
return _PROJECT_ARTIFACT_DELETE(
|
||||||
"artifact_id": artifact_id,
|
{
|
||||||
"version": version,
|
"artifact_id": artifact_id,
|
||||||
"deleted": False,
|
"version": version,
|
||||||
"blocked_by_deployments": blocker_ids,
|
"deleted": False,
|
||||||
}
|
"blocked_by_deployments": blocker_ids,
|
||||||
|
}
|
||||||
|
)
|
||||||
store.delete_artifact(artifact_id, version)
|
store.delete_artifact(artifact_id, version)
|
||||||
self.context.events.record_workflow_event(
|
self.context.events.record_workflow_event(
|
||||||
"workflow_artifact_deleted",
|
"workflow_artifact_deleted",
|
||||||
capability_id=f"{artifact_id}@{version}",
|
capability_id=f"{artifact_id}@{version}",
|
||||||
payload={"artifact_id": artifact_id, "version": version},
|
payload={"artifact_id": artifact_id, "version": version},
|
||||||
)
|
)
|
||||||
return {
|
return _PROJECT_ARTIFACT_DELETE(
|
||||||
"artifact_id": artifact_id,
|
{
|
||||||
"version": version,
|
"artifact_id": artifact_id,
|
||||||
"deleted": True,
|
"version": version,
|
||||||
"blocked_by_deployments": [],
|
"deleted": True,
|
||||||
}
|
"blocked_by_deployments": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _suggested_self_bindings(
|
def _suggested_self_bindings(
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
"""Transport-neutral workflow API models."""
|
"""Transport-neutral workflow API models."""
|
||||||
|
|
||||||
|
from .artifacts import (
|
||||||
|
ArtifactCatalogEntryPayload,
|
||||||
|
ArtifactKindPayload,
|
||||||
|
CapabilityKindPayload,
|
||||||
|
CapabilityRefPayload,
|
||||||
|
DeleteArtifactResult,
|
||||||
|
ListArtifactsResult,
|
||||||
|
RequiredCapabilityPayload,
|
||||||
|
SaveArtifactResult,
|
||||||
|
WorkflowArtifactPayload,
|
||||||
|
)
|
||||||
from .common import (
|
from .common import (
|
||||||
ArtifactVersionPayload,
|
ArtifactVersionPayload,
|
||||||
DependencyDiagnosticPayload,
|
DependencyDiagnosticPayload,
|
||||||
@@ -10,6 +21,7 @@ from .common import (
|
|||||||
JsonSchema,
|
JsonSchema,
|
||||||
NextActionPatchExamplePayload,
|
NextActionPatchExamplePayload,
|
||||||
NextActionsPayload,
|
NextActionsPayload,
|
||||||
|
PageMetadataPayload,
|
||||||
RawWorkflowPlan,
|
RawWorkflowPlan,
|
||||||
TraceRange,
|
TraceRange,
|
||||||
)
|
)
|
||||||
@@ -37,6 +49,11 @@ from .runs import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ArtifactVersionPayload",
|
"ArtifactVersionPayload",
|
||||||
|
"ArtifactCatalogEntryPayload",
|
||||||
|
"ArtifactKindPayload",
|
||||||
|
"CapabilityKindPayload",
|
||||||
|
"CapabilityRefPayload",
|
||||||
|
"DeleteArtifactResult",
|
||||||
"DeleteDeploymentResult",
|
"DeleteDeploymentResult",
|
||||||
"DependencyDiagnosticPayload",
|
"DependencyDiagnosticPayload",
|
||||||
"DeploymentSummary",
|
"DeploymentSummary",
|
||||||
@@ -48,20 +65,25 @@ __all__ = [
|
|||||||
"JsonProjector",
|
"JsonProjector",
|
||||||
"JsonSchema",
|
"JsonSchema",
|
||||||
"ListDeploymentsResult",
|
"ListDeploymentsResult",
|
||||||
|
"ListArtifactsResult",
|
||||||
"ListRunsResult",
|
"ListRunsResult",
|
||||||
"NextActionPatchExamplePayload",
|
"NextActionPatchExamplePayload",
|
||||||
"NextActionsPayload",
|
"NextActionsPayload",
|
||||||
|
"PageMetadataPayload",
|
||||||
"RawWorkflowPlan",
|
"RawWorkflowPlan",
|
||||||
"ResumeReadiness",
|
"ResumeReadiness",
|
||||||
"RunResult",
|
"RunResult",
|
||||||
"RunStatus",
|
"RunStatus",
|
||||||
"RunSummary",
|
"RunSummary",
|
||||||
"RunTraceResult",
|
"RunTraceResult",
|
||||||
|
"RequiredCapabilityPayload",
|
||||||
|
"SaveArtifactResult",
|
||||||
"SaveDeploymentResult",
|
"SaveDeploymentResult",
|
||||||
"SourceBindingPayload",
|
"SourceBindingPayload",
|
||||||
"TraceRange",
|
"TraceRange",
|
||||||
"TraceEntryPayload",
|
"TraceEntryPayload",
|
||||||
"ValidateDeploymentResult",
|
"ValidateDeploymentResult",
|
||||||
"WorkflowDeploymentPayload",
|
"WorkflowDeploymentPayload",
|
||||||
|
"WorkflowArtifactPayload",
|
||||||
"WorkflowRefPayload",
|
"WorkflowRefPayload",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
DependencyDiagnosticPayload,
|
||||||
|
JsonObject,
|
||||||
|
PageMetadataPayload,
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArtifactKindPayload = Literal["workflow", "wrapper"]
|
||||||
|
type CapabilityKindPayload = Literal[
|
||||||
|
"tool",
|
||||||
|
"resource",
|
||||||
|
"prompt",
|
||||||
|
"node_spec",
|
||||||
|
"reducer",
|
||||||
|
"workflow",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityRefPayload(TypedDict):
|
||||||
|
source: str
|
||||||
|
capability_key: str
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredCapabilityPayload(TypedDict):
|
||||||
|
"""Saved dependency contract for one artifact capability reference."""
|
||||||
|
|
||||||
|
ref: CapabilityRefPayload
|
||||||
|
kind: CapabilityKindPayload
|
||||||
|
input_schema_hash: str | None
|
||||||
|
input_schema_snapshot: JsonObject | None
|
||||||
|
output_schema_hash: str | None
|
||||||
|
output_schema_snapshot: JsonObject | None
|
||||||
|
observed_concrete_source: str | None
|
||||||
|
observed_at_epoch_ms: int | None
|
||||||
|
|
||||||
|
|
||||||
|
class ArtifactCatalogEntryPayload(TypedDict):
|
||||||
|
"""Compact artifact row returned by discovery operations."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
artifact_id: str
|
||||||
|
version: int
|
||||||
|
kind: str
|
||||||
|
display_name: str
|
||||||
|
description: str | None
|
||||||
|
outcomes: list[str]
|
||||||
|
input_schema: JsonObject
|
||||||
|
output_schema: JsonObject
|
||||||
|
required_sources: list[str]
|
||||||
|
diagnostics: list[DependencyDiagnosticPayload]
|
||||||
|
|
||||||
|
|
||||||
|
class ListArtifactsResult(PageMetadataPayload):
|
||||||
|
nodes: list[ArtifactCatalogEntryPayload]
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowArtifactPayload(TypedDict):
|
||||||
|
"""Normalized immutable artifact returned by inspect operations."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
version: int
|
||||||
|
title: str
|
||||||
|
kind: ArtifactKindPayload
|
||||||
|
description: str | None
|
||||||
|
input_schema: JsonObject
|
||||||
|
output_schema: JsonObject
|
||||||
|
outcomes: list[str]
|
||||||
|
plan: JsonObject
|
||||||
|
required_capabilities: list[RequiredCapabilityPayload]
|
||||||
|
workflow_dependencies: dict[str, int]
|
||||||
|
created_from_catalog_version: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class SaveArtifactResult(TypedDict):
|
||||||
|
artifact_id: str
|
||||||
|
version: int
|
||||||
|
saved: bool
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteArtifactResult(TypedDict):
|
||||||
|
artifact_id: str
|
||||||
|
version: int
|
||||||
|
deleted: bool
|
||||||
|
blocked_by_deployments: list[str]
|
||||||
@@ -73,6 +73,13 @@ class GuidedResultPayload(TypedDict):
|
|||||||
next_actions: NextActionsPayload
|
next_actions: NextActionsPayload
|
||||||
|
|
||||||
|
|
||||||
|
class PageMetadataPayload(TypedDict):
|
||||||
|
"""Cursor metadata shared by compact workflow discovery responses."""
|
||||||
|
|
||||||
|
next_cursor: str | None
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class TraceRange:
|
class TraceRange:
|
||||||
"""Caller-bounded debug trace slice for durable deployment runs."""
|
"""Caller-bounded debug trace slice for durable deployment runs."""
|
||||||
|
|||||||
@@ -14,14 +14,18 @@ from .draft_authoring import RouteSource, WorkflowDraftAuthoringApi
|
|||||||
from .draft_updates import CapabilityStepUpdate
|
from .draft_updates import CapabilityStepUpdate
|
||||||
from .drafts import WorkflowDraftApi
|
from .drafts import WorkflowDraftApi
|
||||||
from .models import (
|
from .models import (
|
||||||
|
DeleteArtifactResult,
|
||||||
DeleteDeploymentResult,
|
DeleteDeploymentResult,
|
||||||
|
ListArtifactsResult,
|
||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
ListRunsResult,
|
ListRunsResult,
|
||||||
RawWorkflowPlan,
|
RawWorkflowPlan,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
|
SaveArtifactResult,
|
||||||
SaveDeploymentResult,
|
SaveDeploymentResult,
|
||||||
ValidateDeploymentResult,
|
ValidateDeploymentResult,
|
||||||
|
WorkflowArtifactPayload,
|
||||||
WorkflowDeploymentPayload,
|
WorkflowDeploymentPayload,
|
||||||
)
|
)
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
@@ -91,7 +95,7 @@ class WorkflowApi:
|
|||||||
kind: ArtifactKind | None = None,
|
kind: ArtifactKind | None = None,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> ListArtifactsResult:
|
||||||
return await self.artifacts.list_artifacts(
|
return await self.artifacts.list_artifacts(
|
||||||
query=query,
|
query=query,
|
||||||
kind=kind,
|
kind=kind,
|
||||||
@@ -104,7 +108,7 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
version: int,
|
version: int,
|
||||||
) -> dict[str, Any]:
|
) -> WorkflowArtifactPayload:
|
||||||
return await self.artifacts.inspect_artifact(
|
return await self.artifacts.inspect_artifact(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -115,7 +119,7 @@ class WorkflowApi:
|
|||||||
*,
|
*,
|
||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
version: int,
|
version: int,
|
||||||
) -> dict[str, Any]:
|
) -> DeleteArtifactResult:
|
||||||
return await self.artifacts.delete_artifact(
|
return await self.artifacts.delete_artifact(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -124,7 +128,7 @@ class WorkflowApi:
|
|||||||
async def save_artifact(
|
async def save_artifact(
|
||||||
self,
|
self,
|
||||||
artifact: dict[str, Any],
|
artifact: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
return await self.artifacts.save_artifact(artifact)
|
return await self.artifacts.save_artifact(artifact)
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
@@ -140,7 +144,7 @@ class WorkflowApi:
|
|||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
return await self.artifacts.create_artifact_from_plan(
|
return await self.artifacts.create_artifact_from_plan(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
|
|||||||
+13
-4
@@ -10,13 +10,17 @@ from wf_core.models.steps import InputBinding, OutputBinding
|
|||||||
from .draft_authoring import RouteSource
|
from .draft_authoring import RouteSource
|
||||||
from .draft_updates import CapabilityStepUpdate
|
from .draft_updates import CapabilityStepUpdate
|
||||||
from .models import (
|
from .models import (
|
||||||
|
DeleteArtifactResult,
|
||||||
DeleteDeploymentResult,
|
DeleteDeploymentResult,
|
||||||
|
ListArtifactsResult,
|
||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
ListRunsResult,
|
ListRunsResult,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
|
SaveArtifactResult,
|
||||||
SaveDeploymentResult,
|
SaveDeploymentResult,
|
||||||
ValidateDeploymentResult,
|
ValidateDeploymentResult,
|
||||||
|
WorkflowArtifactPayload,
|
||||||
WorkflowDeploymentPayload,
|
WorkflowDeploymentPayload,
|
||||||
)
|
)
|
||||||
from .runs import TraceRangeLike
|
from .runs import TraceRangeLike
|
||||||
@@ -355,21 +359,26 @@ class WorkflowArtifactSurface(Protocol):
|
|||||||
kind: ArtifactKind | None = None,
|
kind: ArtifactKind | None = None,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]: ...
|
) -> ListArtifactsResult: ...
|
||||||
|
|
||||||
async def inspect_artifact(
|
async def inspect_artifact(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
version: int,
|
version: int,
|
||||||
) -> dict[str, Any]: ...
|
) -> WorkflowArtifactPayload: ...
|
||||||
|
|
||||||
|
async def save_artifact(
|
||||||
|
self,
|
||||||
|
artifact: dict[str, Any],
|
||||||
|
) -> SaveArtifactResult: ...
|
||||||
|
|
||||||
async def delete_artifact(
|
async def delete_artifact(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
version: int,
|
version: int,
|
||||||
) -> dict[str, Any]: ...
|
) -> DeleteArtifactResult: ...
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
self,
|
self,
|
||||||
@@ -384,7 +393,7 @@ class WorkflowArtifactSurface(Protocol):
|
|||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]: ...
|
) -> SaveArtifactResult: ...
|
||||||
|
|
||||||
|
|
||||||
class WorkflowDeploymentSurface(Protocol):
|
class WorkflowDeploymentSurface(Protocol):
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal, cast
|
||||||
|
|
||||||
|
from wf_api.models import (
|
||||||
|
DeleteArtifactResult,
|
||||||
|
ListArtifactsResult,
|
||||||
|
SaveArtifactResult,
|
||||||
|
WorkflowArtifactPayload,
|
||||||
|
)
|
||||||
|
|
||||||
from .base import RpcCaller
|
from .base import RpcCaller
|
||||||
|
|
||||||
@@ -16,36 +23,48 @@ class RpcArtifactClientMixin:
|
|||||||
kind: Literal["workflow", "wrapper"] | None = None,
|
kind: Literal["workflow", "wrapper"] | None = None,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> ListArtifactsResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.artifacts.list",
|
ListArtifactsResult,
|
||||||
{
|
await self._call(
|
||||||
"query": query,
|
"workflow.artifacts.list",
|
||||||
"kind": kind,
|
{
|
||||||
"cursor": cursor,
|
"query": query,
|
||||||
"limit": limit,
|
"kind": kind,
|
||||||
},
|
"cursor": cursor,
|
||||||
|
"limit": limit,
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def inspect_artifact(
|
async def inspect_artifact(
|
||||||
self: RpcCaller, *, artifact_id: str, version: int
|
self: RpcCaller, *, artifact_id: str, version: int
|
||||||
) -> dict[str, Any]:
|
) -> WorkflowArtifactPayload:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.artifacts.inspect",
|
WorkflowArtifactPayload,
|
||||||
{"artifact_id": artifact_id, "version": version},
|
await self._call(
|
||||||
|
"workflow.artifacts.inspect",
|
||||||
|
{"artifact_id": artifact_id, "version": version},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def save_artifact(
|
async def save_artifact(
|
||||||
self: RpcCaller, artifact: dict[str, Any]
|
self: RpcCaller, artifact: dict[str, Any]
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
return await self._call("workflow.artifacts.save", {"artifact": artifact})
|
return cast(
|
||||||
|
SaveArtifactResult,
|
||||||
|
await self._call("workflow.artifacts.save", {"artifact": artifact}),
|
||||||
|
)
|
||||||
|
|
||||||
async def delete_artifact(
|
async def delete_artifact(
|
||||||
self: RpcCaller, *, artifact_id: str, version: int
|
self: RpcCaller, *, artifact_id: str, version: int
|
||||||
) -> dict[str, Any]:
|
) -> DeleteArtifactResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.artifacts.delete",
|
DeleteArtifactResult,
|
||||||
{"artifact_id": artifact_id, "version": version},
|
await self._call(
|
||||||
|
"workflow.artifacts.delete",
|
||||||
|
{"artifact_id": artifact_id, "version": version},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
@@ -61,19 +80,22 @@ class RpcArtifactClientMixin:
|
|||||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
source_bindings: dict[str, str] | None = None,
|
source_bindings: dict[str, str] | None = None,
|
||||||
created_from_catalog_version: str | None = None,
|
created_from_catalog_version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.artifacts.create_from_plan",
|
SaveArtifactResult,
|
||||||
{
|
await self._call(
|
||||||
"artifact_id": artifact_id,
|
"workflow.artifacts.create_from_plan",
|
||||||
"version": version,
|
{
|
||||||
"title": title,
|
"artifact_id": artifact_id,
|
||||||
"plan": plan,
|
"version": version,
|
||||||
"outcomes": list(outcomes),
|
"title": title,
|
||||||
"kind": kind,
|
"plan": plan,
|
||||||
"description": description,
|
"outcomes": list(outcomes),
|
||||||
"required_capabilities": required_capabilities,
|
"kind": kind,
|
||||||
"source_bindings": source_bindings,
|
"description": description,
|
||||||
"created_from_catalog_version": created_from_catalog_version,
|
"required_capabilities": required_capabilities,
|
||||||
},
|
"source_bindings": source_bindings,
|
||||||
|
"created_from_catalog_version": created_from_catalog_version,
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
from __future__ import annotations
|
"""Artifact JSON-RPC method registration.
|
||||||
|
|
||||||
from typing import Any
|
Return annotations stay eagerly evaluated because fastapi-jsonrpc captures them
|
||||||
|
while registering nested handlers for response validation and OpenRPC output.
|
||||||
|
"""
|
||||||
|
|
||||||
import fastapi_jsonrpc as jsonrpc
|
import fastapi_jsonrpc as jsonrpc
|
||||||
|
|
||||||
|
from wf_api.models import (
|
||||||
|
DeleteArtifactResult,
|
||||||
|
ListArtifactsResult,
|
||||||
|
SaveArtifactResult,
|
||||||
|
WorkflowArtifactPayload,
|
||||||
|
)
|
||||||
from wf_server import WorkflowServer
|
from wf_server import WorkflowServer
|
||||||
|
|
||||||
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||||
@@ -29,7 +37,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_artifacts_create_from_plan(
|
async def workflow_artifacts_create_from_plan(
|
||||||
params: CreateArtifactFromPlanParams = RpcParams(),
|
params: CreateArtifactFromPlanParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
try:
|
try:
|
||||||
return await server.api.create_artifact_from_plan(
|
return await server.api.create_artifact_from_plan(
|
||||||
artifact_id=params.artifact_id,
|
artifact_id=params.artifact_id,
|
||||||
@@ -49,7 +57,7 @@ def register_methods(
|
|||||||
@entrypoint.method(name="workflow.artifacts.save", errors=[WorkflowRpcError])
|
@entrypoint.method(name="workflow.artifacts.save", errors=[WorkflowRpcError])
|
||||||
async def workflow_artifacts_save(
|
async def workflow_artifacts_save(
|
||||||
params: SaveArtifactParams = RpcParams(),
|
params: SaveArtifactParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> SaveArtifactResult:
|
||||||
try:
|
try:
|
||||||
return await server.api.save_artifact(params.artifact)
|
return await server.api.save_artifact(params.artifact)
|
||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
@@ -58,7 +66,7 @@ def register_methods(
|
|||||||
@entrypoint.method(name="workflow.artifacts.list", errors=[WorkflowRpcError])
|
@entrypoint.method(name="workflow.artifacts.list", errors=[WorkflowRpcError])
|
||||||
async def workflow_artifacts_list(
|
async def workflow_artifacts_list(
|
||||||
params: ListArtifactsParams = RpcParams(),
|
params: ListArtifactsParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> ListArtifactsResult:
|
||||||
try:
|
try:
|
||||||
return await server.api.list_artifacts(
|
return await server.api.list_artifacts(
|
||||||
query=params.query,
|
query=params.query,
|
||||||
@@ -72,7 +80,7 @@ def register_methods(
|
|||||||
@entrypoint.method(name="workflow.artifacts.inspect", errors=[WorkflowRpcError])
|
@entrypoint.method(name="workflow.artifacts.inspect", errors=[WorkflowRpcError])
|
||||||
async def workflow_artifacts_inspect(
|
async def workflow_artifacts_inspect(
|
||||||
params: InspectArtifactParams = RpcParams(),
|
params: InspectArtifactParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> WorkflowArtifactPayload:
|
||||||
try:
|
try:
|
||||||
return await server.api.inspect_artifact(
|
return await server.api.inspect_artifact(
|
||||||
artifact_id=params.artifact_id,
|
artifact_id=params.artifact_id,
|
||||||
@@ -84,7 +92,7 @@ def register_methods(
|
|||||||
@entrypoint.method(name="workflow.artifacts.delete", errors=[WorkflowRpcError])
|
@entrypoint.method(name="workflow.artifacts.delete", errors=[WorkflowRpcError])
|
||||||
async def workflow_artifacts_delete(
|
async def workflow_artifacts_delete(
|
||||||
params: DeleteArtifactParams = RpcParams(),
|
params: DeleteArtifactParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> DeleteArtifactResult:
|
||||||
try:
|
try:
|
||||||
return await server.api.delete_artifact(
|
return await server.api.delete_artifact(
|
||||||
artifact_id=params.artifact_id,
|
artifact_id=params.artifact_id,
|
||||||
|
|||||||
@@ -46,6 +46,59 @@ def test_openrpc_exposes_typed_health_result(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("method_name", "component_name", "properties"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"workflow.artifacts.create_from_plan",
|
||||||
|
"SaveArtifactResult",
|
||||||
|
{"artifact_id", "version", "saved"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workflow.artifacts.save",
|
||||||
|
"SaveArtifactResult",
|
||||||
|
{"artifact_id", "version", "saved"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workflow.artifacts.list",
|
||||||
|
"ListArtifactsResult",
|
||||||
|
{"nodes", "next_cursor", "total"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workflow.artifacts.inspect",
|
||||||
|
"WorkflowArtifactPayload",
|
||||||
|
{
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"title",
|
||||||
|
"kind",
|
||||||
|
"input_schema",
|
||||||
|
"output_schema",
|
||||||
|
"outcomes",
|
||||||
|
"plan",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workflow.artifacts.delete",
|
||||||
|
"DeleteArtifactResult",
|
||||||
|
{"artifact_id", "version", "deleted", "blocked_by_deployments"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_openrpc_exposes_typed_artifact_results(
|
||||||
|
openrpc_document: dict[str, Any],
|
||||||
|
method_name: str,
|
||||||
|
component_name: str,
|
||||||
|
properties: set[str],
|
||||||
|
) -> None:
|
||||||
|
_assert_result_component(
|
||||||
|
openrpc_document,
|
||||||
|
method_name=method_name,
|
||||||
|
component_name=component_name,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("method_name", "component_name", "properties"),
|
("method_name", "component_name", "properties"),
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user