fix: close Python workflow client review
This commit is contained in:
+33
-6
@@ -61,7 +61,7 @@ def _prepare_artifact_from_plan(
|
||||
description: str | None,
|
||||
plan: RawWorkflowPlan | dict[str, Any],
|
||||
outcomes: Sequence[str],
|
||||
required_capabilities: dict[str, dict[str, Any]] | None,
|
||||
required_capabilities: Mapping[str, RequiredCapability | dict[str, Any]] | None,
|
||||
source_bindings: dict[str, str] | None,
|
||||
created_from_catalog_version: str | None,
|
||||
) -> WorkflowArtifact:
|
||||
@@ -80,7 +80,11 @@ def _prepare_artifact_from_plan(
|
||||
plan=typed_plan.model_dump(mode="json", by_alias=True),
|
||||
outcomes=tuple(outcomes),
|
||||
required_capabilities={
|
||||
name: RequiredCapability.model_validate(capability)
|
||||
name: (
|
||||
capability
|
||||
if isinstance(capability, RequiredCapability)
|
||||
else RequiredCapability.model_validate(capability)
|
||||
)
|
||||
for name, capability in (required_capabilities or {}).items()
|
||||
},
|
||||
source_bindings=source_bindings,
|
||||
@@ -103,14 +107,16 @@ def _invalid_artifact_plan_payload(
|
||||
|
||||
def _diagnostic_from_validation_error(
|
||||
exc: ValidationError,
|
||||
*,
|
||||
root: str = "plan",
|
||||
) -> ArtifactPlanDiagnosticPayload:
|
||||
"""Project the first typed model error with a stable plan-rooted path."""
|
||||
"""Project the first typed model error beneath its request-field root."""
|
||||
error = exc.errors()[0]
|
||||
location = ".".join(str(part) for part in error["loc"])
|
||||
return {
|
||||
"severity": "error",
|
||||
"code": "artifact_plan_invalid",
|
||||
"path": f"plan.{location}" if location else "plan",
|
||||
"path": f"{root}.{location}" if location else root,
|
||||
"message": str(error["msg"]),
|
||||
"repair_hint": None,
|
||||
}
|
||||
@@ -254,6 +260,27 @@ class WorkflowArtifactApi:
|
||||
source_bindings: dict[str, str] | None = None,
|
||||
) -> ValidateArtifactPlanResult:
|
||||
"""Validate and inventory a plan without writing the artifact store."""
|
||||
try:
|
||||
typed_plan = RawWorkflowPlan.model_validate(plan)
|
||||
except ValidationError as exc:
|
||||
return _PROJECT_VALIDATE_ARTIFACT(
|
||||
_invalid_artifact_plan_payload(_diagnostic_from_validation_error(exc))
|
||||
)
|
||||
|
||||
typed_requirements: dict[str, RequiredCapability] = {}
|
||||
for name, capability in (required_capabilities or {}).items():
|
||||
try:
|
||||
typed_requirements[name] = RequiredCapability.model_validate(capability)
|
||||
except ValidationError as exc:
|
||||
return _PROJECT_VALIDATE_ARTIFACT(
|
||||
_invalid_artifact_plan_payload(
|
||||
_diagnostic_from_validation_error(
|
||||
exc,
|
||||
root=f"required_capabilities.{name}",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# These identity fields satisfy the shared artifact factory only;
|
||||
# validation never calls the store or emits a saved-artifact event.
|
||||
@@ -264,9 +291,9 @@ class WorkflowArtifactApi:
|
||||
title="Validation",
|
||||
kind="workflow",
|
||||
description=None,
|
||||
plan=plan,
|
||||
plan=typed_plan,
|
||||
outcomes=outcomes,
|
||||
required_capabilities=required_capabilities,
|
||||
required_capabilities=typed_requirements,
|
||||
source_bindings=source_bindings,
|
||||
created_from_catalog_version=None,
|
||||
)
|
||||
|
||||
@@ -107,6 +107,13 @@ def _workflow_dependencies_from_plan(plan: JsonObject) -> dict[str, int]:
|
||||
continue
|
||||
workflow_ref = WorkflowRef.model_validate(node.get("workflow"))
|
||||
if workflow_ref.artifact_id is not None and workflow_ref.version is not None:
|
||||
pinned = dependencies.get(workflow_ref.artifact_id)
|
||||
if pinned is not None and pinned != workflow_ref.version:
|
||||
raise WorkflowPlanValidationError(
|
||||
"invalid workflow plan: conflicting versions "
|
||||
f"{pinned} and {workflow_ref.version} pinned for child "
|
||||
f"artifact {workflow_ref.artifact_id!r}"
|
||||
)
|
||||
dependencies[workflow_ref.artifact_id] = workflow_ref.version
|
||||
return dependencies
|
||||
|
||||
|
||||
@@ -29,12 +29,8 @@ from wf_transport_rpc_http.client.base import RpcProtocolError
|
||||
|
||||
from .errors import (
|
||||
ArtifactNotFound,
|
||||
ArtifactVersionConflict,
|
||||
CapabilityNotFound,
|
||||
DeploymentNotRunnable,
|
||||
DeploymentRequired,
|
||||
ProtocolError,
|
||||
RevisionConflict,
|
||||
TransportError,
|
||||
WorkflowClientError,
|
||||
)
|
||||
@@ -64,22 +60,8 @@ def _known_protocol_error(
|
||||
operation: str,
|
||||
error: RpcProtocolError,
|
||||
) -> WorkflowClientError | None:
|
||||
"""Translate only stable codes or exact legacy missing-resource signals."""
|
||||
"""Translate only operation-specific errors emitted by the current server."""
|
||||
code, detail = _server_detail(error)
|
||||
normalized = code.casefold() if code is not None else ""
|
||||
if normalized in {"capability_not_found", "capabilitynotfound"}:
|
||||
return CapabilityNotFound(detail)
|
||||
if normalized in {"artifact_not_found", "artifactnotfound"}:
|
||||
return ArtifactNotFound(detail)
|
||||
if normalized in {"artifact_version_conflict", "artifactversionconflict"}:
|
||||
return ArtifactVersionConflict(detail)
|
||||
if normalized in {"revision_conflict", "revisionconflict"}:
|
||||
return RevisionConflict(detail)
|
||||
if normalized in {"deployment_required", "deploymentrequired"}:
|
||||
return DeploymentRequired()
|
||||
if normalized in {"deployment_not_runnable", "deploymentnotrunnable"}:
|
||||
return DeploymentNotRunnable(error=detail)
|
||||
|
||||
# The current RPC server reports expected application exception class names
|
||||
# in ``data.code``. A generic KeyError is safe to specialize only when both
|
||||
# the operation and its exact resource phrase agree.
|
||||
@@ -87,11 +69,11 @@ def _known_protocol_error(
|
||||
if operation.startswith("workflow.capabilities.") and (
|
||||
"unknown workflow capability" in detail
|
||||
):
|
||||
return CapabilityNotFound(detail)
|
||||
return CapabilityNotFound(detail, code=error.code, data=error.data)
|
||||
if operation == "workflow.artifacts.inspect" and (
|
||||
"unknown workflow artifact" in detail
|
||||
):
|
||||
return ArtifactNotFound(detail)
|
||||
return ArtifactNotFound(detail, code=error.code, data=error.data)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class Deployment:
|
||||
deployment_id=self.deployment_id,
|
||||
artifact=f"{self.artifact_id}.v{self.artifact_version}",
|
||||
runnable=self.runnable,
|
||||
diagnostics=f"{len(self.diagnostics)} diagnostics",
|
||||
diagnostics=f"{len(self._diagnostics)} diagnostics",
|
||||
)
|
||||
|
||||
def _repr_html_(self) -> str:
|
||||
@@ -126,7 +126,7 @@ class Deployment:
|
||||
artifact=f"{self.artifact_id}.v{self.artifact_version}",
|
||||
bindings=f"{len(self.bindings)} bindings",
|
||||
runnable=self.runnable,
|
||||
diagnostics=f"{len(self.diagnostics)} diagnostics",
|
||||
diagnostics=f"{len(self._diagnostics)} diagnostics",
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
+16
-3
@@ -11,6 +11,20 @@ from wf_artifacts import DependencyDiagnostic
|
||||
class WorkflowClientError(Exception):
|
||||
"""Base class for errors that can be handled by workflow callers."""
|
||||
|
||||
code: int | str | None
|
||||
data: object
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "",
|
||||
*,
|
||||
code: int | str | None = None,
|
||||
data: object = None,
|
||||
) -> None:
|
||||
self.code = code
|
||||
self.data = deepcopy(data)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class TransportError(WorkflowClientError):
|
||||
"""The client could not communicate with the workflow service."""
|
||||
@@ -29,10 +43,9 @@ class ProtocolError(WorkflowClientError):
|
||||
message: str,
|
||||
data: object = None,
|
||||
) -> None:
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.data = deepcopy(data)
|
||||
super().__init__(str(self))
|
||||
super().__init__(message, code=code, data=data)
|
||||
self.args = (str(self),)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if isinstance(self.data, dict) and isinstance(self.data.get("message"), str):
|
||||
|
||||
@@ -51,9 +51,10 @@ class RpcClientTransport:
|
||||
http_client: httpx.AsyncClient | None = None
|
||||
|
||||
async def _call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
request_id = uuid4().hex
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": uuid4().hex,
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
@@ -66,6 +67,10 @@ class RpcClientTransport:
|
||||
payload = response.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError("JSON-RPC response must be an object")
|
||||
if payload.get("jsonrpc") != "2.0":
|
||||
raise RuntimeError("JSON-RPC response must declare version '2.0'")
|
||||
if payload.get("id") != request_id:
|
||||
raise RuntimeError("JSON-RPC response id does not match the request")
|
||||
if "error" in payload:
|
||||
error = payload["error"]
|
||||
if not isinstance(error, dict):
|
||||
|
||||
Reference in New Issue
Block a user