feat: deliver Python workflow client

This commit is contained in:
lda
2026-08-31 02:49:07 +07:00 Verified
parent d53b96fd7c
commit 5315d4b66e
18 changed files with 717 additions and 19 deletions
+103
View File
@@ -0,0 +1,103 @@
"""Small, inert renderers shared by the public workflow-client snapshots.
Representations are a debugging aid, not another client operation. This
module deliberately accepts already-loaded values and never knows about the
workflow transport port. The same bounded projector is used for plain and
HTML representations so notebooks cannot accidentally expose an unbounded
trace, output, or credential-shaped value.
"""
from __future__ import annotations
import html
import json
from collections.abc import Mapping, Sequence
_SECRET_KEY_PARTS = (
"authorization",
"cookie",
"set-cookie",
"token",
"secret",
"password",
"api_key",
"api-key",
)
_MAX_DEPTH = 2
_MAX_ITEMS = 8
_MAX_STRING = 160
_MAX_RENDERED = 1_200
def _secret_key(key: object) -> bool:
lowered = str(key).lower()
return any(part in lowered for part in _SECRET_KEY_PARTS)
def bounded_value(value: object, *, depth: int = 0) -> object:
"""Project loaded JSON-like data into a small, secret-safe preview."""
if depth >= _MAX_DEPTH:
return "[truncated]"
if isinstance(value, str):
return value if len(value) <= _MAX_STRING else value[:_MAX_STRING] + ""
if value is None or isinstance(value, bool | int | float):
return value
if isinstance(value, Mapping):
items = list(value.items())
preview = {
str(key): "[redacted]"
if _secret_key(key)
else bounded_value(item, depth=depth + 1)
for key, item in items[:_MAX_ITEMS]
}
if len(items) > _MAX_ITEMS:
preview[""] = f"{len(items) - _MAX_ITEMS} more entries"
return preview
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
items = list(value)
preview = [bounded_value(item, depth=depth + 1) for item in items[:_MAX_ITEMS]]
if len(items) > _MAX_ITEMS:
preview.append(f"{len(items) - _MAX_ITEMS} more items")
return preview
rendered = repr(value)
return rendered if len(rendered) <= _MAX_STRING else rendered[:_MAX_STRING] + ""
def preview(value: object) -> str:
"""Render a bounded value without allowing an object's repr to grow freely."""
try:
rendered = json.dumps(bounded_value(value), sort_keys=True, default=str)
except TypeError, ValueError:
rendered = str(bounded_value(value))
return rendered[:_MAX_RENDERED] + ("" if len(rendered) > _MAX_RENDERED else "")
def short_repr(type_name: str, **fields: object) -> str:
"""Build a compact Python repr from already-loaded field values."""
body = ", ".join(f"{name}={preview(value)}" for name, value in fields.items())
rendered = f"{type_name}({body})"
return rendered[:_MAX_RENDERED] + ("" if len(rendered) > _MAX_RENDERED else "")
def html_repr(type_name: str, **fields: object) -> str:
"""Build a bounded HTML table suitable for IPython rich display."""
def html_preview(value: object) -> str:
rendered = preview(value)
return rendered[:400] + ("" if len(rendered) > 400 else "")
rows = "".join(
'<tr><th scope="row">'
+ html.escape(name)
+ "</th><td><code>"
+ html.escape(html_preview(value))
+ "</code></td></tr>"
for name, value in fields.items()
)
return (
'<div class="wf-client-repr"><strong>'
+ html.escape(type_name)
+ "</strong><table><tbody>"
+ rows
+ "</tbody></table></div>"
)
+56
View File
@@ -13,6 +13,7 @@ from wf_artifacts.models import DependencyDiagnostic
from wf_core.models.schemas import NodeDef, SchemaRef
from wf_platform import CapabilityRef
from ._repr import html_repr, short_repr
from .codec import decode_capability_call, decode_capability_diagnostics
from .errors import InvalidResponse
from .protocols import WorkflowClientPort
@@ -39,6 +40,24 @@ class CapabilitySummary:
"""Compatibility alias for the wire row's ``name`` field."""
return self.qualified_name
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
name=self.qualified_name,
source=self.source_id,
outcomes=self.outcomes,
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
name=self.qualified_name,
source=self.source_id,
outcomes=self.outcomes,
inputs=f"{len(self.input_fields)} fields",
outputs=f"{len(self.output_fields)} fields",
)
@dataclass(frozen=True, slots=True)
class CapabilityResult:
@@ -48,6 +67,22 @@ class CapabilityResult:
output: dict[str, Any] | None
diagnostics: tuple[DependencyDiagnostic, ...]
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
outcome=self.outcome,
output=self.output,
diagnostics=f"{len(self.diagnostics)} diagnostics",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
outcome=self.outcome,
output=self.output,
diagnostics=f"{len(self.diagnostics)} diagnostics",
)
def _check_schema(schema: object, *, operation: str) -> dict[str, Any]:
if not isinstance(schema, Mapping):
@@ -106,6 +141,27 @@ class RemoteCapability:
)
object.__setattr__(self, "outcomes", tuple(self.outcomes))
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
name=self.qualified_name,
outcomes=self.outcomes,
input_schema=f"{len(self.input_schema)} keys",
output_schema=f"{len(self.output_schema)} keys",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
name=self.qualified_name,
description=self.description,
outcomes=self.outcomes,
**{
"input schema": f"{len(self.input_schema)} keys",
"output schema": f"{len(self.output_schema)} keys",
},
)
async def __call__(
self,
payload: Mapping[str, Any] | None = None,
+36
View File
@@ -8,6 +8,7 @@ from typing import Any
from wf_artifacts import DependencyDiagnostic, DriftPolicy, WorkflowDeployment
from ._repr import html_repr, short_repr
from .codec import (
decode_dependency_diagnostics,
decode_deployment,
@@ -33,6 +34,22 @@ class DeploymentValidation:
def runnable(self) -> bool:
return self.status == "runnable"
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
deployment_id=self.deployment_id,
status=self.status,
diagnostics=f"{len(self.diagnostics)} diagnostics",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
deployment_id=self.deployment_id,
status=self.status,
diagnostics=f"{len(self.diagnostics)} diagnostics",
)
@dataclass(frozen=True, slots=True)
class Deployment:
@@ -51,6 +68,25 @@ class Deployment:
def deployment_id(self) -> str:
return self.model.id
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
deployment_id=self.deployment_id,
artifact=f"{self.artifact_id}.v{self.artifact_version}",
runnable=self.runnable,
diagnostics=f"{len(self.diagnostics)} diagnostics",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
deployment_id=self.deployment_id,
artifact=f"{self.artifact_id}.v{self.artifact_version}",
bindings=f"{len(self.bindings)} bindings",
runnable=self.runnable,
diagnostics=f"{len(self.diagnostics)} diagnostics",
)
@property
def artifact_id(self) -> str:
return self.model.artifact_id
+43
View File
@@ -12,6 +12,7 @@ from wf_api import TraceRange
from wf_artifacts import DependencyDiagnostic
from wf_core import InterruptRequest, InterruptRoute, TraceEntry, WorkflowRef
from ._repr import html_repr, short_repr
from .codec import DecodedRunResult, decode_run_result, decode_trace_result
from .errors import DeploymentNotRunnable, InvalidResponse
from .protocols import WorkflowClientPort
@@ -27,6 +28,24 @@ class TracePage:
truncated: bool
trace_count: int
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
start=self.start,
limit=self.limit,
frames=f"{len(self.frames)} loaded/{self.trace_count} total",
truncated=self.truncated,
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
start=self.start,
limit=self.limit,
frames=f"{len(self.frames)} loaded/{self.trace_count} total",
truncated=self.truncated,
)
def _interrupt(
payload: Mapping[str, Any] | None,
@@ -110,6 +129,30 @@ class Run:
diagnostics: tuple[DependencyDiagnostic, ...]
trace_count: int
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
run_id=self.run_id,
deployment_id=self.deployment_id,
status=self.status,
outcome=self.outcome,
output=self.output,
diagnostics=f"{len(self.diagnostics)} diagnostics",
trace=f"{self.trace_count} frames",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
run_id=self.run_id,
deployment_id=self.deployment_id,
status=self.status,
outcome=self.outcome,
output=self.output,
diagnostics=f"{len(self.diagnostics)} diagnostics",
trace=f"{self.trace_count} frames (use trace() for a bounded page)",
)
@classmethod
def from_payload(
cls,
+69 -2
View File
@@ -14,6 +14,7 @@ from wf_artifacts.models import (
)
from wf_core import ValidationReport, Workflow
from ._repr import html_repr, short_repr
from .errors import InvalidResponse, ValidationFailed
if TYPE_CHECKING:
@@ -30,6 +31,16 @@ class ArtifactRef:
artifact_id: str
version: int
def __repr__(self) -> str:
return short_repr(
type(self).__name__, artifact_id=self.artifact_id, version=self.version
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__, artifact_id=self.artifact_id, version=self.version
)
@dataclass(frozen=True, slots=True)
class WorkflowDiagnostic:
@@ -41,6 +52,24 @@ class WorkflowDiagnostic:
message: str
repair_hint: str | None = None
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
severity=self.severity,
code=self.code,
path=self.path,
message=self.message,
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
severity=self.severity,
code=self.code,
path=self.path,
message=self.message,
)
# Keep the short name used by the public design available without requiring a
# second diagnostic implementation.
@@ -59,6 +88,22 @@ class WorkflowValidation:
def ok(self) -> bool:
return self.local.ok and self.remote_status == "valid"
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
ok=self.ok,
remote_status=self.remote_status,
diagnostics=f"{len(self.remote_diagnostics)} diagnostics",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
ok=self.ok,
remote_status=self.remote_status,
diagnostics=f"{len(self.remote_diagnostics)} diagnostics",
)
def raise_for_errors(self) -> None:
"""Raise a useful error for either local or remote validation failures."""
self.local.raise_for_errors()
@@ -86,6 +131,23 @@ class WorkflowArtifact:
def ref(self) -> ArtifactRef:
return ArtifactRef(self.artifact.id, self.artifact.version)
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
ref=self.ref,
title=self.title,
required_capabilities=f"{len(self.required_capabilities)} capabilities",
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
ref=self.ref,
title=self.title,
description=self.description,
required_capabilities=f"{len(self.required_capabilities)} capabilities",
)
@property
def title(self) -> str:
return self.artifact.title
@@ -137,8 +199,13 @@ class WorkflowArtifact:
"drift_policy": drift_policy,
}
)
if not isinstance(saved, Mapping) or saved.get("deployment_id") != deployment_id:
saved_id = saved.get("deployment_id") if isinstance(saved, Mapping) else None
if (
not isinstance(saved, Mapping)
or saved.get("deployment_id") != deployment_id
):
saved_id = (
saved.get("deployment_id") if isinstance(saved, Mapping) else None
)
raise InvalidResponse(
operation="workflow.deployments.save",
details=(