feat: add remote workflow lifecycle rpc methods
This commit is contained in:
@@ -94,9 +94,9 @@ implementation state.
|
||||
`WorkflowServer` through fixed dotted methods. Remote CLI targeting remains
|
||||
the next transport-facing slice.
|
||||
- Completed: workflow config now distinguishes client targets from server
|
||||
hosting config, selected `wf cap`/`wf run` commands can target JSON-RPC
|
||||
HTTP with explicit CLI overrides, and local-only draft/artifact/deploy
|
||||
commands fail fast for remote targets until they are wired.
|
||||
hosting config, the basic `wf` lifecycle can target JSON-RPC HTTP:
|
||||
capability discovery, draft workspace authoring, artifact/deployment
|
||||
operations, run, inspect, and bounded trace.
|
||||
|
||||
5. **CLI/API alignment**
|
||||
- Let the CLI target either local process-backed stores/runtime or the future
|
||||
|
||||
@@ -462,8 +462,9 @@ First slice implemented:
|
||||
wired source ids (`wf.std`, `wf.recipes`)
|
||||
- local and JSON-RPC client targets
|
||||
- `wf` root overrides for `--local`, `--url`, and `--timeout`
|
||||
- remote JSON-RPC client support for capability and run CLI commands
|
||||
- local-only CLI commands fail fast for `rpc_http` targets until they are wired
|
||||
- remote JSON-RPC client support now covers capability, draft workspace,
|
||||
artifact, deployment, and run CLI commands
|
||||
- draft/artifact/deploy commands no longer fail fast for `rpc_http` targets
|
||||
- `wf-rpc-server --config` support for server store and RPC HTTP transport,
|
||||
including configured RPC path
|
||||
|
||||
@@ -473,7 +474,6 @@ Still future:
|
||||
- MCP/OpenAPI source config
|
||||
- arbitrary stdlib source aliases
|
||||
- `/mcp` hosting from neutral server config
|
||||
- remote draft/artifact/deployment CLI commands
|
||||
- auth and SQL stores
|
||||
|
||||
## Next Implementation Slice
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Annotated, Literal
|
||||
|
||||
import typer
|
||||
|
||||
from wf_cli.context import load_local_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.context import load_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.formats import ListOutputFormat, emit_list_payload
|
||||
from wf_cli.io import emit_json
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from wf_cli.context import load_local_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.context import load_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.formats import ListOutputFormat, emit_list_payload
|
||||
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_input
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Annotated, Literal
|
||||
|
||||
import typer
|
||||
|
||||
from wf_cli.context import load_local_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.context import load_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.formats import ListOutputFormat, emit_list_payload
|
||||
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value
|
||||
|
||||
|
||||
@@ -4,12 +4,22 @@ from .app import create_rpc_app
|
||||
from .client import RpcWorkflowApiClient
|
||||
from .errors import WorkflowRpcError
|
||||
from .models import (
|
||||
CreateArtifactFromWorkspaceParams,
|
||||
CreateDraftFromCapabilityParams,
|
||||
CreateWrapperFromWorkspaceParams,
|
||||
DeleteDeploymentParams,
|
||||
GetDraftWorkspaceParams,
|
||||
HealthParams,
|
||||
InspectArtifactParams,
|
||||
InspectCapabilityParams,
|
||||
InspectDeploymentParams,
|
||||
InspectRunParams,
|
||||
ListArtifactsParams,
|
||||
ListCapabilitiesParams,
|
||||
ListDeploymentsParams,
|
||||
ListDraftWorkspacesParams,
|
||||
PatchDraftParams,
|
||||
PatchDraftWorkspaceParams,
|
||||
ReadRunTraceParams,
|
||||
ResumeRunParams,
|
||||
SaveArtifactParams,
|
||||
@@ -18,15 +28,26 @@ from .models import (
|
||||
TraceRangeParams,
|
||||
ValidateDeploymentParams,
|
||||
ValidateDraftParams,
|
||||
ValidateDraftWorkspaceParams,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CreateArtifactFromWorkspaceParams",
|
||||
"CreateDraftFromCapabilityParams",
|
||||
"CreateWrapperFromWorkspaceParams",
|
||||
"DeleteDeploymentParams",
|
||||
"GetDraftWorkspaceParams",
|
||||
"HealthParams",
|
||||
"InspectArtifactParams",
|
||||
"InspectCapabilityParams",
|
||||
"InspectDeploymentParams",
|
||||
"InspectRunParams",
|
||||
"ListArtifactsParams",
|
||||
"ListCapabilitiesParams",
|
||||
"ListDeploymentsParams",
|
||||
"ListDraftWorkspacesParams",
|
||||
"PatchDraftParams",
|
||||
"PatchDraftWorkspaceParams",
|
||||
"ReadRunTraceParams",
|
||||
"ResumeRunParams",
|
||||
"SaveArtifactParams",
|
||||
@@ -35,6 +56,7 @@ __all__ = [
|
||||
"TraceRangeParams",
|
||||
"ValidateDeploymentParams",
|
||||
"ValidateDraftParams",
|
||||
"ValidateDraftWorkspaceParams",
|
||||
"WorkflowRpcError",
|
||||
"create_rpc_app",
|
||||
"RpcWorkflowApiClient",
|
||||
|
||||
@@ -10,11 +10,21 @@ from wf_server import WorkflowServer
|
||||
|
||||
from .errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||
from .models import (
|
||||
CreateArtifactFromWorkspaceParams,
|
||||
CreateDraftFromCapabilityParams,
|
||||
CreateWrapperFromWorkspaceParams,
|
||||
DeleteDeploymentParams,
|
||||
GetDraftWorkspaceParams,
|
||||
InspectArtifactParams,
|
||||
InspectCapabilityParams,
|
||||
InspectDeploymentParams,
|
||||
InspectRunParams,
|
||||
ListArtifactsParams,
|
||||
ListCapabilitiesParams,
|
||||
ListDeploymentsParams,
|
||||
ListDraftWorkspacesParams,
|
||||
PatchDraftParams,
|
||||
PatchDraftWorkspaceParams,
|
||||
ReadRunTraceParams,
|
||||
ResumeRunParams,
|
||||
SaveArtifactParams,
|
||||
@@ -22,6 +32,7 @@ from .models import (
|
||||
StartRunParams,
|
||||
ValidateDeploymentParams,
|
||||
ValidateDraftParams,
|
||||
ValidateDraftWorkspaceParams,
|
||||
)
|
||||
|
||||
|
||||
@@ -116,6 +127,125 @@ def create_rpc_app(server: WorkflowServer, *, rpc_path: str = "/rpc") -> jsonrpc
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.draft_workspaces.list", errors=[WorkflowRpcError])
|
||||
async def workflow_draft_workspaces_list(
|
||||
params: ListDraftWorkspacesParams = Body(
|
||||
default_factory=ListDraftWorkspacesParams
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.list_draft_workspaces()
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.draft_workspaces.get", errors=[WorkflowRpcError])
|
||||
async def workflow_draft_workspaces_get(
|
||||
params: GetDraftWorkspaceParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.get_draft_workspace(
|
||||
workspace_id=params.workspace_id,
|
||||
include_draft=params.include_draft,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.create_from_capability",
|
||||
errors=[WorkflowRpcError],
|
||||
)
|
||||
async def workflow_draft_workspaces_create_from_capability(
|
||||
params: CreateDraftFromCapabilityParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.create_draft_workspace_from_capability(
|
||||
workspace_id=params.workspace_id,
|
||||
capability_name=params.capability_name,
|
||||
name=params.name,
|
||||
title=params.title,
|
||||
input_schema=params.input_schema,
|
||||
state_schema=params.state_schema,
|
||||
output_schema=params.output_schema,
|
||||
input=params.input,
|
||||
output=params.output,
|
||||
input_map=params.input_map,
|
||||
output_map=params.output_map,
|
||||
error_message_source=params.error_message_source,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.patch", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_patch(
|
||||
params: PatchDraftWorkspaceParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.patch_draft_workspace(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
patch=params.patch,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_validate(
|
||||
params: ValidateDraftWorkspaceParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.validate_draft_workspace(
|
||||
workspace_id=params.workspace_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.create_artifact", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_create_artifact(
|
||||
params: CreateArtifactFromWorkspaceParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.create_artifact_from_workspace(
|
||||
workspace_id=params.workspace_id,
|
||||
artifact_id=params.artifact_id,
|
||||
version=params.version,
|
||||
title=params.title,
|
||||
outcomes=tuple(params.outcomes),
|
||||
kind=params.kind,
|
||||
description=params.description,
|
||||
required_capabilities=params.required_capabilities,
|
||||
source_bindings=params.source_bindings,
|
||||
created_from_catalog_version=params.created_from_catalog_version,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.create_wrapper", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_create_wrapper(
|
||||
params: CreateWrapperFromWorkspaceParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.create_wrapper_from_workspace(
|
||||
workspace_id=params.workspace_id,
|
||||
artifact_id=params.artifact_id,
|
||||
version=params.version,
|
||||
title=params.title,
|
||||
outcomes=tuple(params.outcomes),
|
||||
description=params.description,
|
||||
required_capabilities=params.required_capabilities,
|
||||
source_bindings=params.source_bindings,
|
||||
created_from_catalog_version=params.created_from_catalog_version,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.artifacts.save", errors=[WorkflowRpcError])
|
||||
async def workflow_artifacts_save(
|
||||
params: SaveArtifactParams = Params(...), # type: ignore[reportArgumentType],
|
||||
@@ -146,6 +276,63 @@ def create_rpc_app(server: WorkflowServer, *, rpc_path: str = "/rpc") -> jsonrpc
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.artifacts.list", errors=[WorkflowRpcError])
|
||||
async def workflow_artifacts_list(
|
||||
params: ListArtifactsParams = Body(default_factory=ListArtifactsParams),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.list_artifacts(
|
||||
query=params.query,
|
||||
kind=params.kind,
|
||||
cursor=params.cursor,
|
||||
limit=params.limit,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.artifacts.inspect", errors=[WorkflowRpcError])
|
||||
async def workflow_artifacts_inspect(
|
||||
params: InspectArtifactParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.inspect_artifact(
|
||||
artifact_id=params.artifact_id,
|
||||
version=params.version,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.deployments.list", errors=[WorkflowRpcError])
|
||||
async def workflow_deployments_list(
|
||||
params: ListDeploymentsParams = Body(default_factory=ListDeploymentsParams),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.list_deployments()
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.deployments.inspect", errors=[WorkflowRpcError])
|
||||
async def workflow_deployments_inspect(
|
||||
params: InspectDeploymentParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.inspect_deployment(
|
||||
deployment_id=params.deployment_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.deployments.delete", errors=[WorkflowRpcError])
|
||||
async def workflow_deployments_delete(
|
||||
params: DeleteDeploymentParams = Params(...), # type: ignore[reportArgumentType],
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.delete_deployment(
|
||||
deployment_id=params.deployment_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.runs.start", errors=[WorkflowRpcError])
|
||||
async def workflow_runs_start(
|
||||
params: StartRunParams = Params(...), # type: ignore[reportArgumentType],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
@@ -14,7 +15,7 @@ class RpcWorkflowApiClient:
|
||||
"""Small WorkflowApi-compatible adapter for JSON-RPC HTTP targets.
|
||||
|
||||
This is intentionally not a full WorkflowApi clone. It implements only the
|
||||
methods used by the first remote CLI slice.
|
||||
methods used by CLI commands targeting rpc_http transports.
|
||||
"""
|
||||
|
||||
url: str
|
||||
@@ -47,6 +48,8 @@ class RpcWorkflowApiClient:
|
||||
raise RuntimeError("JSON-RPC response result must be an object")
|
||||
return result
|
||||
|
||||
# -- capabilities --
|
||||
|
||||
async def list_capabilities(
|
||||
self,
|
||||
*,
|
||||
@@ -71,6 +74,197 @@ class RpcWorkflowApiClient:
|
||||
{"qualified_name": qualified_name},
|
||||
)
|
||||
|
||||
# -- draft workspaces --
|
||||
|
||||
async def list_draft_workspaces(self) -> dict[str, Any]:
|
||||
return await self._call("workflow.draft_workspaces.list", {})
|
||||
|
||||
async def get_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
include_draft: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.get",
|
||||
{"workspace_id": workspace_id, "include_draft": include_draft},
|
||||
)
|
||||
|
||||
async def create_draft_workspace_from_capability(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
capability_name: str,
|
||||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
input_schema: dict[str, Any] | None = None,
|
||||
state_schema: dict[str, Any] | None = None,
|
||||
output_schema: dict[str, Any] | None = None,
|
||||
input: Sequence[Any] | None = None,
|
||||
output: Sequence[Any] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
output_map: dict[str, str] | None = None,
|
||||
error_message_source: Any | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.create_from_capability",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"capability_name": capability_name,
|
||||
"name": name,
|
||||
"title": title,
|
||||
"input_schema": input_schema,
|
||||
"state_schema": state_schema,
|
||||
"output_schema": output_schema,
|
||||
"input": input,
|
||||
"output": output,
|
||||
"input_map": input_map,
|
||||
"output_map": output_map,
|
||||
"error_message_source": error_message_source,
|
||||
},
|
||||
)
|
||||
|
||||
async def patch_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
patch: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.patch",
|
||||
{"workspace_id": workspace_id, "revision": revision, "patch": patch},
|
||||
)
|
||||
|
||||
async def validate_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.validate",
|
||||
{"workspace_id": workspace_id},
|
||||
)
|
||||
|
||||
async def create_artifact_from_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
artifact_id: str,
|
||||
version: int,
|
||||
title: str,
|
||||
outcomes: Sequence[str],
|
||||
kind: Literal["workflow", "wrapper"] = "workflow",
|
||||
description: str | None = None,
|
||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||
source_bindings: dict[str, str] | None = None,
|
||||
created_from_catalog_version: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.create_artifact",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"artifact_id": artifact_id,
|
||||
"version": version,
|
||||
"title": title,
|
||||
"outcomes": list(outcomes),
|
||||
"kind": kind,
|
||||
"description": description,
|
||||
"required_capabilities": required_capabilities,
|
||||
"source_bindings": source_bindings,
|
||||
"created_from_catalog_version": created_from_catalog_version,
|
||||
},
|
||||
)
|
||||
|
||||
async def create_wrapper_from_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
artifact_id: str,
|
||||
version: int,
|
||||
title: str,
|
||||
outcomes: Sequence[str],
|
||||
description: str | None = None,
|
||||
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||
source_bindings: dict[str, str] | None = None,
|
||||
created_from_catalog_version: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.create_wrapper",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"artifact_id": artifact_id,
|
||||
"version": version,
|
||||
"title": title,
|
||||
"outcomes": list(outcomes),
|
||||
"description": description,
|
||||
"required_capabilities": required_capabilities,
|
||||
"source_bindings": source_bindings,
|
||||
"created_from_catalog_version": created_from_catalog_version,
|
||||
},
|
||||
)
|
||||
|
||||
# -- artifacts --
|
||||
|
||||
async def list_artifacts(
|
||||
self,
|
||||
*,
|
||||
query: str | None = None,
|
||||
kind: Literal["workflow", "wrapper"] | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.artifacts.list",
|
||||
{
|
||||
"query": query,
|
||||
"kind": kind,
|
||||
"cursor": cursor,
|
||||
"limit": limit,
|
||||
},
|
||||
)
|
||||
|
||||
async def inspect_artifact(
|
||||
self, *, artifact_id: str, version: int
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.artifacts.inspect",
|
||||
{"artifact_id": artifact_id, "version": version},
|
||||
)
|
||||
|
||||
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
|
||||
return await self._call("workflow.artifacts.save", {"artifact": artifact})
|
||||
|
||||
# -- deployments --
|
||||
|
||||
async def list_deployments(self) -> dict[str, Any]:
|
||||
return await self._call("workflow.deployments.list", {})
|
||||
|
||||
async def inspect_deployment(self, *, deployment_id: str) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.deployments.inspect",
|
||||
{"deployment_id": deployment_id},
|
||||
)
|
||||
|
||||
async def validate_deployment(
|
||||
self, *, deployment_id: str, live_check: bool = False
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.deployments.validate",
|
||||
{"deployment_id": deployment_id, "live_check": live_check},
|
||||
)
|
||||
|
||||
async def save_deployment(self, deployment: dict[str, Any]) -> dict[str, Any]:
|
||||
return await self._call("workflow.deployments.save", {"deployment": deployment})
|
||||
|
||||
async def delete_deployment(self, *, deployment_id: str) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.deployments.delete",
|
||||
{"deployment_id": deployment_id},
|
||||
)
|
||||
|
||||
# -- runs --
|
||||
|
||||
async def run_deployment(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -73,6 +73,74 @@ class SaveDeploymentParams(RpcParamsModel):
|
||||
deployment: dict[str, Any]
|
||||
|
||||
|
||||
class ListDraftWorkspacesParams(RpcParamsModel):
|
||||
pass
|
||||
|
||||
|
||||
class GetDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
include_draft: bool = False
|
||||
|
||||
|
||||
class PatchDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
patch: list[dict[str, Any]]
|
||||
|
||||
|
||||
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class CreateArtifactFromWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
artifact_id: str = Field(min_length=1)
|
||||
version: int = Field(ge=1)
|
||||
title: str = Field(min_length=1)
|
||||
outcomes: list[str]
|
||||
kind: Literal["workflow", "wrapper"] = "workflow"
|
||||
description: str | None = None
|
||||
required_capabilities: dict[str, dict[str, Any]] | None = None
|
||||
source_bindings: dict[str, str] | None = None
|
||||
created_from_catalog_version: str | None = None
|
||||
|
||||
|
||||
class CreateWrapperFromWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
artifact_id: str = Field(min_length=1)
|
||||
version: int = Field(ge=1)
|
||||
title: str = Field(min_length=1)
|
||||
outcomes: list[str]
|
||||
description: str | None = None
|
||||
required_capabilities: dict[str, dict[str, Any]] | None = None
|
||||
source_bindings: dict[str, str] | None = None
|
||||
created_from_catalog_version: str | None = None
|
||||
|
||||
|
||||
class ListArtifactsParams(RpcParamsModel):
|
||||
query: str | None = None
|
||||
kind: Literal["workflow", "wrapper"] | None = None
|
||||
cursor: str | None = None
|
||||
limit: int = Field(default=50, ge=1, le=100)
|
||||
|
||||
|
||||
class InspectArtifactParams(RpcParamsModel):
|
||||
artifact_id: str = Field(min_length=1)
|
||||
version: int = Field(ge=1)
|
||||
|
||||
|
||||
class ListDeploymentsParams(RpcParamsModel):
|
||||
pass
|
||||
|
||||
|
||||
class InspectDeploymentParams(RpcParamsModel):
|
||||
deployment_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class DeleteDeploymentParams(RpcParamsModel):
|
||||
deployment_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ValidateDeploymentParams(RpcParamsModel):
|
||||
deployment_id: str = Field(min_length=1)
|
||||
live_check: bool = False
|
||||
|
||||
@@ -247,3 +247,97 @@ def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert '"name": "wf.std.constant"' in result.output
|
||||
|
||||
|
||||
def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
original_client = httpx.AsyncClient
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.client.httpx.AsyncClient",
|
||||
lambda *args, **kwargs: original_client(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
base_url="http://test",
|
||||
),
|
||||
)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
runner = CliRunner()
|
||||
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
|
||||
|
||||
created = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"draft",
|
||||
"create-from-capability",
|
||||
"remote_ws",
|
||||
"wf.std.constant",
|
||||
"--name",
|
||||
"remote_constant",
|
||||
"--title",
|
||||
"Remote Constant",
|
||||
],
|
||||
)
|
||||
assert created.exit_code == 0, created.output
|
||||
assert '"workspace_id": "remote_ws"' in created.output
|
||||
|
||||
validated = runner.invoke(
|
||||
app,
|
||||
[*base_args, "draft", "validate", "remote_ws"],
|
||||
)
|
||||
assert validated.exit_code == 0, validated.output
|
||||
assert '"status": "valid"' in validated.output
|
||||
|
||||
saved_artifact = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"draft",
|
||||
"save",
|
||||
"remote_ws",
|
||||
"--artifact",
|
||||
"remote_artifact",
|
||||
"--version",
|
||||
"1",
|
||||
"--title",
|
||||
"Remote Artifact",
|
||||
"--outcome",
|
||||
"ok",
|
||||
"--binding",
|
||||
"wf.std=wf.std",
|
||||
],
|
||||
)
|
||||
assert saved_artifact.exit_code == 0, saved_artifact.output
|
||||
assert '"artifact_id": "remote_artifact"' in saved_artifact.output
|
||||
|
||||
inspected_artifact = runner.invoke(
|
||||
app,
|
||||
[*base_args, "artifact", "inspect", "remote_artifact", "1"],
|
||||
)
|
||||
assert inspected_artifact.exit_code == 0, inspected_artifact.output
|
||||
assert '"id": "remote_artifact"' in inspected_artifact.output
|
||||
|
||||
saved_deployment = runner.invoke(
|
||||
app,
|
||||
[
|
||||
*base_args,
|
||||
"deploy",
|
||||
"save",
|
||||
"remote_artifact.default",
|
||||
"--artifact",
|
||||
"remote_artifact",
|
||||
"--version",
|
||||
"1",
|
||||
"--binding",
|
||||
"wf.std=wf.std",
|
||||
],
|
||||
)
|
||||
assert saved_deployment.exit_code == 0, saved_deployment.output
|
||||
assert '"deployment_id": "remote_artifact.default"' in saved_deployment.output
|
||||
|
||||
validated_deployment = runner.invoke(
|
||||
app,
|
||||
[*base_args, "deploy", "validate", "remote_artifact.default"],
|
||||
)
|
||||
assert validated_deployment.exit_code == 0, validated_deployment.output
|
||||
assert '"status": "runnable"' in validated_deployment.output
|
||||
|
||||
@@ -211,6 +211,124 @@ def test_rpc_draft_artifact_deployment_lifecycle(tmp_path) -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_rpc_artifact_and_deployment_catalog_methods(tmp_path) -> None:
|
||||
async def scenario() -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
await server.api.create_artifact_from_plan(
|
||||
artifact_id="rpc_lifecycle",
|
||||
version=1,
|
||||
title="RPC Lifecycle",
|
||||
plan=_constant_plan(),
|
||||
outcomes=["ok"],
|
||||
source_bindings={"wf.std": "wf.std"},
|
||||
)
|
||||
await server.api.save_deployment(
|
||||
{
|
||||
"id": "rpc_lifecycle.default",
|
||||
"artifact_id": "rpc_lifecycle",
|
||||
"artifact_version": 1,
|
||||
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
||||
}
|
||||
)
|
||||
|
||||
app = create_rpc_app(server)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
) as client:
|
||||
listed_artifacts = await _rpc(client, "workflow.artifacts.list", {})
|
||||
inspected_artifact = await _rpc(
|
||||
client,
|
||||
"workflow.artifacts.inspect",
|
||||
{"artifact_id": "rpc_lifecycle", "version": 1},
|
||||
)
|
||||
listed_deployments = await _rpc(client, "workflow.deployments.list", {})
|
||||
inspected_deployment = await _rpc(
|
||||
client,
|
||||
"workflow.deployments.inspect",
|
||||
{"deployment_id": "rpc_lifecycle.default"},
|
||||
)
|
||||
deleted = await _rpc(
|
||||
client,
|
||||
"workflow.deployments.delete",
|
||||
{"deployment_id": "rpc_lifecycle.default"},
|
||||
)
|
||||
|
||||
assert listed_artifacts["result"]["nodes"]
|
||||
assert inspected_artifact["result"]["id"] == "rpc_lifecycle"
|
||||
assert listed_deployments["result"]["deployments"]
|
||||
assert inspected_deployment["result"]["id"] == "rpc_lifecycle.default"
|
||||
assert deleted["result"]["deployment_id"] == "rpc_lifecycle.default"
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_rpc_draft_workspace_methods(tmp_path) -> None:
|
||||
async def scenario() -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
) as client:
|
||||
created = await _rpc(
|
||||
client,
|
||||
"workflow.draft_workspaces.create_from_capability",
|
||||
{
|
||||
"workspace_id": "remote_ws",
|
||||
"capability_name": "wf.std.constant",
|
||||
"name": "remote_constant",
|
||||
"title": "Remote Constant",
|
||||
"input_map": {},
|
||||
"output_map": {"value": "state.result"},
|
||||
},
|
||||
)
|
||||
listed = await _rpc(client, "workflow.draft_workspaces.list", {})
|
||||
fetched = await _rpc(
|
||||
client,
|
||||
"workflow.draft_workspaces.get",
|
||||
{"workspace_id": "remote_ws"},
|
||||
)
|
||||
validated = await _rpc(
|
||||
client,
|
||||
"workflow.draft_workspaces.validate",
|
||||
{"workspace_id": "remote_ws"},
|
||||
)
|
||||
patched = await _rpc(
|
||||
client,
|
||||
"workflow.draft_workspaces.patch",
|
||||
{
|
||||
"workspace_id": "remote_ws",
|
||||
"revision": created["result"]["revision"],
|
||||
"patch": [
|
||||
{"op": "replace", "path": "/name", "value": "remote_renamed"}
|
||||
],
|
||||
},
|
||||
)
|
||||
artifact = await _rpc(
|
||||
client,
|
||||
"workflow.draft_workspaces.create_artifact",
|
||||
{
|
||||
"workspace_id": "remote_ws",
|
||||
"artifact_id": "remote_artifact",
|
||||
"version": 1,
|
||||
"title": "Remote Artifact",
|
||||
"outcomes": ["ok"],
|
||||
"kind": "workflow",
|
||||
"source_bindings": {"wf.std": "wf.std"},
|
||||
},
|
||||
)
|
||||
|
||||
assert created["result"]["workspace_id"] == "remote_ws"
|
||||
assert listed["result"]["workspaces"]
|
||||
assert fetched["result"]["workspace_id"] == "remote_ws"
|
||||
assert validated["result"]["status"] in {"valid", "invalid"}
|
||||
assert patched["result"]["revision"] == created["result"]["revision"] + 1
|
||||
assert artifact["result"]["artifact_id"] == "remote_artifact"
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def _constant_plan() -> RawWorkflowPlan:
|
||||
return RawWorkflowPlan.model_validate(
|
||||
{
|
||||
|
||||
@@ -157,3 +157,128 @@ def test_rpc_workflow_client_raises_for_rpc_error(tmp_path) -> None:
|
||||
assert "missing.capability" in message
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_rpc_workflow_client_lists_and_inspects_artifacts(tmp_path) -> None:
|
||||
async def scenario() -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
await server.api.create_artifact_from_plan(
|
||||
artifact_id="client_art",
|
||||
version=1,
|
||||
title="Client Art",
|
||||
plan=_constant_plan(),
|
||||
outcomes=["ok"],
|
||||
source_bindings={"wf.std": "wf.std"},
|
||||
)
|
||||
app = create_rpc_app(server)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(
|
||||
url="http://test/rpc", timeout_seconds=5, http_client=http_client
|
||||
)
|
||||
listed = await client.list_artifacts()
|
||||
inspected = await client.inspect_artifact(
|
||||
artifact_id="client_art", version=1
|
||||
)
|
||||
|
||||
assert listed["nodes"]
|
||||
assert inspected["id"] == "client_art"
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_rpc_workflow_client_lists_inspects_validates_and_deletes_deployments(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
async def scenario() -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
await server.api.create_artifact_from_plan(
|
||||
artifact_id="client_deploy_art",
|
||||
version=1,
|
||||
title="Client Deploy Art",
|
||||
plan=_constant_plan(),
|
||||
outcomes=["ok"],
|
||||
source_bindings={"wf.std": "wf.std"},
|
||||
)
|
||||
await server.api.save_deployment(
|
||||
{
|
||||
"id": "client_deploy_art.default",
|
||||
"artifact_id": "client_deploy_art",
|
||||
"artifact_version": 1,
|
||||
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
||||
}
|
||||
)
|
||||
app = create_rpc_app(server)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(
|
||||
url="http://test/rpc", timeout_seconds=5, http_client=http_client
|
||||
)
|
||||
listed = await client.list_deployments()
|
||||
inspected = await client.inspect_deployment(
|
||||
deployment_id="client_deploy_art.default"
|
||||
)
|
||||
validated = await client.validate_deployment(
|
||||
deployment_id="client_deploy_art.default"
|
||||
)
|
||||
deleted = await client.delete_deployment(
|
||||
deployment_id="client_deploy_art.default"
|
||||
)
|
||||
|
||||
assert listed["deployments"]
|
||||
assert inspected["id"] == "client_deploy_art.default"
|
||||
assert validated["status"] == "runnable"
|
||||
assert deleted["deployment_id"] == "client_deploy_art.default"
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_rpc_workflow_client_draft_workspace_lifecycle(tmp_path) -> None:
|
||||
async def scenario() -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(
|
||||
url="http://test/rpc", timeout_seconds=5, http_client=http_client
|
||||
)
|
||||
created = await client.create_draft_workspace_from_capability(
|
||||
workspace_id="client_ws",
|
||||
capability_name="wf.std.constant",
|
||||
name="client_constant",
|
||||
title="Client Constant",
|
||||
input_map={},
|
||||
output_map={"value": "state.result"},
|
||||
)
|
||||
listed = await client.list_draft_workspaces()
|
||||
fetched = await client.get_draft_workspace(workspace_id="client_ws")
|
||||
validated = await client.validate_draft_workspace(workspace_id="client_ws")
|
||||
patched = await client.patch_draft_workspace(
|
||||
workspace_id="client_ws",
|
||||
revision=created["revision"],
|
||||
patch=[{"op": "replace", "path": "/name", "value": "client_renamed"}],
|
||||
)
|
||||
artifact = await client.create_artifact_from_workspace(
|
||||
workspace_id="client_ws",
|
||||
artifact_id="client_ws_art",
|
||||
version=1,
|
||||
title="Client WS Art",
|
||||
outcomes=("ok",),
|
||||
kind="workflow",
|
||||
source_bindings={"wf.std": "wf.std"},
|
||||
)
|
||||
|
||||
assert created["workspace_id"] == "client_ws"
|
||||
assert listed["workspaces"]
|
||||
assert fetched["workspace_id"] == "client_ws"
|
||||
assert validated["status"] in {"valid", "invalid"}
|
||||
assert patched["revision"] == created["revision"] + 1
|
||||
assert artifact["artifact_id"] == "client_ws_art"
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
Reference in New Issue
Block a user