sched: schedule RPC methods, transport/client decoding, round trips (T13)

This commit is contained in:
lda
2026-09-09 11:49:35 +07:00 Verified
parent 7f00422fd7
commit 2c84aba96b
16 changed files with 1717 additions and 0 deletions
+2
View File
@@ -21,6 +21,7 @@ from .errors import (
WorkflowClientError,
)
from .runs import Run, TracePage
from .schedules import Schedule
from .workflows import (
ArtifactRef,
Diagnostic,
@@ -52,6 +53,7 @@ __all__ = [
"Run",
"RunSummary",
"RevisionConflict",
"Schedule",
"TransportError",
"ValidationFailed",
"WorkflowClientError",
+115
View File
@@ -16,10 +16,13 @@ from wf_api.models import (
ListCapabilitiesResult,
ListDeploymentsResult,
ListRunsResult,
ListSchedulesResult,
OccurrencePage,
RunResult,
RunTraceResult,
SaveArtifactResult,
SaveDeploymentResult,
ScheduleResult,
ValidateArtifactPlanResult,
ValidateDeploymentResult,
WorkflowArtifactPayload,
@@ -318,3 +321,115 @@ class PublicErrorWorkflowClientPort:
run_id=run_id,
trace_range=trace_range,
)
async def create_schedule(
self,
*,
schedule_id: str,
deployment_id: str,
trigger: dict[str, Any],
input_bindings: list[dict[str, Any]] | None = None,
overlap: str = "skip",
misfire: str = "skip",
max_active_runs: int = 1,
lateness_allowance_s: float = 60.0,
max_steps: int | None = None,
enabled: bool = True,
) -> ScheduleResult:
return await self._invoke(
"workflow.schedules.create",
self._rpc.create_schedule,
schedule_id=schedule_id,
deployment_id=deployment_id,
trigger=trigger,
input_bindings=input_bindings,
overlap=overlap,
misfire=misfire,
max_active_runs=max_active_runs,
lateness_allowance_s=lateness_allowance_s,
max_steps=max_steps,
enabled=enabled,
)
async def get_schedule(self, *, schedule_id: str) -> ScheduleResult:
return await self._invoke(
"workflow.schedules.get",
self._rpc.get_schedule,
schedule_id=schedule_id,
)
async def list_schedules(
self, *, include_deleted: bool = False
) -> ListSchedulesResult:
return await self._invoke(
"workflow.schedules.list",
self._rpc.list_schedules,
include_deleted=include_deleted,
)
async def update_schedule(
self,
*,
schedule_id: str,
expected_revision: int,
deployment_id: str | None = None,
trigger: dict[str, Any] | None = None,
input_bindings: list[dict[str, Any]] | None = None,
overlap: str | None = None,
misfire: str | None = None,
max_active_runs: int | None = None,
lateness_allowance_s: float | None = None,
max_steps: int | None = None,
enabled: bool | None = None,
) -> ScheduleResult:
return await self._invoke(
"workflow.schedules.update",
self._rpc.update_schedule,
schedule_id=schedule_id,
expected_revision=expected_revision,
deployment_id=deployment_id,
trigger=trigger,
input_bindings=input_bindings,
overlap=overlap,
misfire=misfire,
max_active_runs=max_active_runs,
lateness_allowance_s=lateness_allowance_s,
max_steps=max_steps,
enabled=enabled,
)
async def pause_schedule(self, *, schedule_id: str) -> ScheduleResult:
return await self._invoke(
"workflow.schedules.pause",
self._rpc.pause_schedule,
schedule_id=schedule_id,
)
async def resume_schedule(self, *, schedule_id: str) -> ScheduleResult:
return await self._invoke(
"workflow.schedules.resume",
self._rpc.resume_schedule,
schedule_id=schedule_id,
)
async def delete_schedule(self, *, schedule_id: str) -> ScheduleResult:
return await self._invoke(
"workflow.schedules.delete",
self._rpc.delete_schedule,
schedule_id=schedule_id,
)
async def list_schedule_occurrences(
self,
*,
schedule_id: str,
cursor: str | None = None,
limit: int = 50,
) -> OccurrencePage:
return await self._invoke(
"workflow.schedules.occurrences.list",
self._rpc.list_schedule_occurrences,
schedule_id=schedule_id,
cursor=cursor,
limit=limit,
)
+153
View File
@@ -18,7 +18,9 @@ from .codec import (
decode_capabilities_page,
decode_capability_inspect,
decode_deployments,
decode_occurrence_page,
decode_runs_page,
decode_schedule_list,
decode_workflow_artifact,
)
from .discovery import ArtifactSummary, DeploymentSummary, RunSummary
@@ -27,8 +29,11 @@ from .protocols import WorkflowClientPort
from .workflows import WorkflowArtifact
if TYPE_CHECKING:
from wf_api.models import OccurrencePage
from .deployments import Deployment
from .runs import Run
from .schedules import Schedule
def _capability_ref(qualified_name: str, source_id: str) -> CapabilityRef:
@@ -298,3 +303,151 @@ class App:
expected_run_id=run_id,
operation="workflow.runs.inspect",
)
async def create_schedule(
self,
*,
schedule_id: str,
deployment_id: str,
trigger: dict[str, Any],
input_bindings: list[dict[str, Any]] | None = None,
overlap: str = "skip",
misfire: str = "skip",
max_active_runs: int = 1,
lateness_allowance_s: float = 60.0,
max_steps: int | None = None,
enabled: bool = True,
) -> Schedule:
"""Create one schedule and return its immutable snapshot."""
from .schedules import Schedule
return Schedule.from_payload(
self._port,
await self._port.create_schedule(
schedule_id=schedule_id,
deployment_id=deployment_id,
trigger=trigger,
input_bindings=input_bindings,
overlap=overlap,
misfire=misfire,
max_active_runs=max_active_runs,
lateness_allowance_s=lateness_allowance_s,
max_steps=max_steps,
enabled=enabled,
),
expected_schedule_id=schedule_id,
operation="workflow.schedules.create",
)
async def schedule(self, schedule_id: str) -> Schedule:
"""Inspect and reconstruct one immutable schedule snapshot."""
from .schedules import Schedule
return Schedule.from_payload(
self._port,
await self._port.get_schedule(schedule_id=schedule_id),
expected_schedule_id=schedule_id,
operation="workflow.schedules.get",
)
async def schedules(self, *, include_deleted: bool = False) -> tuple[Schedule, ...]:
"""List schedule snapshots ordered by id (deleted excluded by default)."""
from .schedules import Schedule
wire = decode_schedule_list(
await self._port.list_schedules(include_deleted=include_deleted)
)
return tuple(
Schedule.from_payload(
self._port,
row,
operation="workflow.schedules.list",
)
for row in wire["schedules"]
)
async def update_schedule(
self,
*,
schedule_id: str,
expected_revision: int,
deployment_id: str | None = None,
trigger: dict[str, Any] | None = None,
input_bindings: list[dict[str, Any]] | None = None,
overlap: str | None = None,
misfire: str | None = None,
max_active_runs: int | None = None,
lateness_allowance_s: float | None = None,
max_steps: int | None = None,
enabled: bool | None = None,
) -> Schedule:
"""Apply a revision-checked schedule edit; ``None`` leaves a field unpatched."""
from .schedules import Schedule
return Schedule.from_payload(
self._port,
await self._port.update_schedule(
schedule_id=schedule_id,
expected_revision=expected_revision,
deployment_id=deployment_id,
trigger=trigger,
input_bindings=input_bindings,
overlap=overlap,
misfire=misfire,
max_active_runs=max_active_runs,
lateness_allowance_s=lateness_allowance_s,
max_steps=max_steps,
enabled=enabled,
),
expected_schedule_id=schedule_id,
operation="workflow.schedules.update",
)
async def pause_schedule(self, schedule_id: str) -> Schedule:
"""Pause one schedule and return its new snapshot."""
from .schedules import Schedule
return Schedule.from_payload(
self._port,
await self._port.pause_schedule(schedule_id=schedule_id),
expected_schedule_id=schedule_id,
operation="workflow.schedules.pause",
)
async def resume_schedule(self, schedule_id: str) -> Schedule:
"""Resume one schedule and return its new snapshot."""
from .schedules import Schedule
return Schedule.from_payload(
self._port,
await self._port.resume_schedule(schedule_id=schedule_id),
expected_schedule_id=schedule_id,
operation="workflow.schedules.resume",
)
async def delete_schedule(self, schedule_id: str) -> Schedule:
"""Soft-delete one schedule and return its new snapshot."""
from .schedules import Schedule
return Schedule.from_payload(
self._port,
await self._port.delete_schedule(schedule_id=schedule_id),
expected_schedule_id=schedule_id,
operation="workflow.schedules.delete",
)
async def schedule_occurrences(
self,
schedule_id: str,
*,
cursor: str | None = None,
limit: int = 50,
) -> OccurrencePage:
"""Return one validated occurrence-history page for a schedule."""
return decode_occurrence_page(
await self._port.list_schedule_occurrences(
schedule_id=schedule_id,
cursor=cursor,
limit=limit,
)
)
+30
View File
@@ -16,11 +16,14 @@ from wf_api.models import (
ListCapabilitiesResult,
ListDeploymentsResult,
ListRunsResult,
ListSchedulesResult,
OccurrencePage,
RawWorkflowPlan,
RunResult,
RunTraceResult,
SaveArtifactResult,
SaveDeploymentResult,
ScheduleResult,
ValidateArtifactPlanResult,
ValidateDeploymentResult,
WorkflowArtifactPayload,
@@ -304,3 +307,30 @@ def decode_trace_result(payload: object) -> DecodedTracePage:
return DecodedTracePage(
*(getattr(fields, field.name) for field in dataclass_fields(_DecodedRunFields))
)
def decode_schedule_result(
payload: object,
*,
operation: str = "workflow.schedules.get",
) -> ScheduleResult:
"""Validate one schedule definition response at the client boundary."""
return _validate(payload, ScheduleResult, operation)
def decode_schedule_list(
payload: object,
*,
operation: str = "workflow.schedules.list",
) -> ListSchedulesResult:
"""Validate one schedule listing response at the client boundary."""
return _validate(payload, ListSchedulesResult, operation)
def decode_occurrence_page(
payload: object,
*,
operation: str = "workflow.schedules.occurrences.list",
) -> OccurrencePage:
"""Validate one occurrence-history page at the client boundary."""
return _validate(payload, OccurrencePage, operation)
+72
View File
@@ -12,10 +12,13 @@ from wf_api.models import (
ListCapabilitiesResult,
ListDeploymentsResult,
ListRunsResult,
ListSchedulesResult,
OccurrencePage,
RunResult,
RunTraceResult,
SaveArtifactResult,
SaveDeploymentResult,
ScheduleResult,
ValidateArtifactPlanResult,
ValidateDeploymentResult,
WorkflowArtifactPayload,
@@ -149,3 +152,72 @@ class WorkflowClientPort(Protocol):
run_id: str,
trace_range: TraceRangeLike,
) -> RunTraceResult: ...
async def create_schedule(
self,
*,
schedule_id: str,
deployment_id: str,
trigger: dict[str, Any],
input_bindings: list[dict[str, Any]] | None = None,
overlap: str = "skip",
misfire: str = "skip",
max_active_runs: int = 1,
lateness_allowance_s: float = 60.0,
max_steps: int | None = None,
enabled: bool = True,
) -> ScheduleResult: ...
async def get_schedule(
self,
*,
schedule_id: str,
) -> ScheduleResult: ...
async def list_schedules(
self,
*,
include_deleted: bool = False,
) -> ListSchedulesResult: ...
async def update_schedule(
self,
*,
schedule_id: str,
expected_revision: int,
deployment_id: str | None = None,
trigger: dict[str, Any] | None = None,
input_bindings: list[dict[str, Any]] | None = None,
overlap: str | None = None,
misfire: str | None = None,
max_active_runs: int | None = None,
lateness_allowance_s: float | None = None,
max_steps: int | None = None,
enabled: bool | None = None,
) -> ScheduleResult: ...
async def pause_schedule(
self,
*,
schedule_id: str,
) -> ScheduleResult: ...
async def resume_schedule(
self,
*,
schedule_id: str,
) -> ScheduleResult: ...
async def delete_schedule(
self,
*,
schedule_id: str,
) -> ScheduleResult: ...
async def list_schedule_occurrences(
self,
*,
schedule_id: str,
cursor: str | None = None,
limit: int = 50,
) -> OccurrencePage: ...
+152
View File
@@ -0,0 +1,152 @@
"""Immutable snapshots for schedule administration definitions."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any
from ._identity import require_response_identity
from ._repr import html_repr, short_repr
from .codec import decode_schedule_result
from .protocols import WorkflowClientPort
@dataclass(frozen=True, slots=True, init=False)
class Schedule:
"""Immutable client snapshot of one schedule definition.
The snapshot keeps the validated wire values (trigger and JSON data
bindings are plain data, never transport DTO instances) and reloads
through ``refresh``. State transitions (update/pause/resume/delete)
stay on the ``App`` facade so this snapshot remains a minimal,
read-plus-refresh mirror of the ``Run`` surface.
"""
_port: WorkflowClientPort = field(repr=False, compare=False)
schedule_id: str
deployment_id: str
_trigger: dict[str, Any] = field(repr=False)
_input_bindings: list[dict[str, Any]] = field(repr=False)
revision: int
enabled: bool
paused: bool
deleted: bool
overlap: str
misfire: str
max_active_runs: int
lateness_allowance_s: float
max_steps: int | None
def __init__(
self,
*,
_port: WorkflowClientPort,
schedule_id: str,
deployment_id: str,
trigger: dict[str, Any],
input_bindings: list[dict[str, Any]],
revision: int,
enabled: bool,
paused: bool,
deleted: bool,
overlap: str,
misfire: str,
max_active_runs: int,
lateness_allowance_s: float,
max_steps: int | None,
) -> None:
object.__setattr__(self, "_port", _port)
object.__setattr__(self, "schedule_id", schedule_id)
object.__setattr__(self, "deployment_id", deployment_id)
object.__setattr__(self, "_trigger", deepcopy(trigger))
object.__setattr__(self, "_input_bindings", deepcopy(input_bindings))
object.__setattr__(self, "revision", revision)
object.__setattr__(self, "enabled", enabled)
object.__setattr__(self, "paused", paused)
object.__setattr__(self, "deleted", deleted)
object.__setattr__(self, "overlap", overlap)
object.__setattr__(self, "misfire", misfire)
object.__setattr__(self, "max_active_runs", max_active_runs)
object.__setattr__(self, "lateness_allowance_s", lateness_allowance_s)
object.__setattr__(self, "max_steps", max_steps)
@property
def id(self) -> str:
"""Return the schedule id (alias matching the wire ``id`` field)."""
return self.schedule_id
@property
def trigger(self) -> dict[str, Any]:
"""Return a defensive copy of the schedule trigger definition."""
return deepcopy(self._trigger)
@property
def input_bindings(self) -> list[dict[str, Any]]:
"""Return a defensive copy of the schedule JSON data bindings."""
return deepcopy(self._input_bindings)
def __repr__(self) -> str:
return short_repr(
type(self).__name__,
schedule_id=self.schedule_id,
deployment_id=self.deployment_id,
revision=self.revision,
enabled=self.enabled,
paused=self.paused,
deleted=self.deleted,
)
def _repr_html_(self) -> str:
return html_repr(
type(self).__name__,
schedule_id=self.schedule_id,
deployment_id=self.deployment_id,
revision=self.revision,
enabled=self.enabled,
paused=self.paused,
deleted=self.deleted,
)
@classmethod
def from_payload(
cls,
port: WorkflowClientPort,
payload: object,
*,
expected_schedule_id: str | None = None,
operation: str = "workflow.schedules.get",
) -> Schedule:
"""Validate one schedule response and reconstruct its snapshot."""
wire = decode_schedule_result(payload, operation=operation)
if expected_schedule_id is not None:
require_response_identity(
operation=operation,
actual={"schedule_id": wire["id"]},
expected={"schedule_id": expected_schedule_id},
)
return cls(
_port=port,
schedule_id=wire["id"],
deployment_id=wire["deployment_id"],
trigger=dict(wire["trigger"]),
input_bindings=[dict(binding) for binding in wire["input_bindings"]],
revision=wire["revision"],
enabled=wire["enabled"],
paused=wire["paused"],
deleted=wire["deleted"],
overlap=wire["overlap"],
misfire=wire["misfire"],
max_active_runs=wire["max_active_runs"],
lateness_allowance_s=wire["lateness_allowance_s"],
max_steps=wire["max_steps"],
)
async def refresh(self) -> Schedule:
"""Read the current server snapshot without mutating this schedule."""
return self.from_payload(
self._port,
await self._port.get_schedule(schedule_id=self.schedule_id),
expected_schedule_id=self.schedule_id,
operation="workflow.schedules.get",
)