sched: schedule RPC methods, transport/client decoding, round trips (T13)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -15,9 +15,12 @@ from .models import (
|
||||
CreateArtifactFromWorkspaceParams,
|
||||
CreateDraftFromCapabilityParams,
|
||||
CreateEmptyDraftWorkspaceParams,
|
||||
CreateScheduleParams,
|
||||
CreateWrapperFromWorkspaceParams,
|
||||
DeleteDeploymentParams,
|
||||
DeleteScheduleParams,
|
||||
GetDraftWorkspaceParams,
|
||||
GetScheduleParams,
|
||||
HandleDraftBranch,
|
||||
HandleDraftParams,
|
||||
HealthParams,
|
||||
@@ -30,15 +33,19 @@ from .models import (
|
||||
ListCapabilitiesParams,
|
||||
ListDeploymentsParams,
|
||||
ListDraftWorkspacesParams,
|
||||
ListOccurrencesParams,
|
||||
ListSchedulesParams,
|
||||
ListSourcesParams,
|
||||
PatchDraftParams,
|
||||
PatchDraftWorkspaceParams,
|
||||
PauseScheduleParams,
|
||||
ReadRunTraceParams,
|
||||
RemoveDraftBindingParams,
|
||||
RemoveDraftRouteParams,
|
||||
RemoveDraftStepParams,
|
||||
ReplaceDraftWorkspaceDocumentParams,
|
||||
ResumeRunParams,
|
||||
ResumeScheduleParams,
|
||||
RouteSourceParams,
|
||||
SaveArtifactParams,
|
||||
SaveDeploymentParams,
|
||||
@@ -55,6 +62,7 @@ from .models import (
|
||||
StartRunParams,
|
||||
TraceRangeParams,
|
||||
UpdateCapabilityStepParams,
|
||||
UpdateScheduleParams,
|
||||
ValidateDeploymentParams,
|
||||
ValidateDraftParams,
|
||||
ValidateDraftWorkspaceParams,
|
||||
@@ -72,9 +80,12 @@ __all__ = [
|
||||
"CreateArtifactFromWorkspaceParams",
|
||||
"CreateDraftFromCapabilityParams",
|
||||
"CreateEmptyDraftWorkspaceParams",
|
||||
"CreateScheduleParams",
|
||||
"CreateWrapperFromWorkspaceParams",
|
||||
"DeleteDeploymentParams",
|
||||
"DeleteScheduleParams",
|
||||
"GetDraftWorkspaceParams",
|
||||
"GetScheduleParams",
|
||||
"HandleDraftBranch",
|
||||
"HandleDraftParams",
|
||||
"HealthParams",
|
||||
@@ -87,15 +98,19 @@ __all__ = [
|
||||
"ListCapabilitiesParams",
|
||||
"ListDeploymentsParams",
|
||||
"ListDraftWorkspacesParams",
|
||||
"ListOccurrencesParams",
|
||||
"ListSchedulesParams",
|
||||
"ListSourcesParams",
|
||||
"PatchDraftParams",
|
||||
"PatchDraftWorkspaceParams",
|
||||
"PauseScheduleParams",
|
||||
"ReadRunTraceParams",
|
||||
"RemoveDraftBindingParams",
|
||||
"RemoveDraftRouteParams",
|
||||
"RemoveDraftStepParams",
|
||||
"ReplaceDraftWorkspaceDocumentParams",
|
||||
"ResumeRunParams",
|
||||
"ResumeScheduleParams",
|
||||
"RouteSourceParams",
|
||||
"RpcWorkflowApiClient",
|
||||
"SaveArtifactParams",
|
||||
@@ -113,6 +128,7 @@ __all__ = [
|
||||
"StartRunParams",
|
||||
"TraceRangeParams",
|
||||
"UpdateCapabilityStepParams",
|
||||
"UpdateScheduleParams",
|
||||
"ValidateDeploymentParams",
|
||||
"ValidateDraftParams",
|
||||
"ValidateDraftWorkspaceParams",
|
||||
|
||||
@@ -20,6 +20,7 @@ from .methods.capabilities import (
|
||||
from .methods.deployments import register_methods as register_deployment_methods
|
||||
from .methods.drafts import register_methods as register_draft_methods
|
||||
from .methods.runs import register_methods as register_run_methods
|
||||
from .methods.schedules import register_methods as register_schedule_methods
|
||||
from .methods.source_registry import (
|
||||
register_methods as register_source_registry_methods,
|
||||
)
|
||||
@@ -65,6 +66,7 @@ def create_rpc_app(
|
||||
register_artifact_methods(entrypoint, server)
|
||||
register_deployment_methods(entrypoint, server)
|
||||
register_run_methods(entrypoint, server)
|
||||
register_schedule_methods(entrypoint, server)
|
||||
register_source_methods(entrypoint, server)
|
||||
register_source_registry_methods(entrypoint, server)
|
||||
register_admin_methods(entrypoint, server)
|
||||
|
||||
@@ -9,6 +9,7 @@ from .capabilities import RpcCapabilityClientMixin
|
||||
from .deployments import RpcDeploymentClientMixin
|
||||
from .drafts import RpcDraftClientMixin
|
||||
from .runs import RpcRunClientMixin
|
||||
from .schedules import RpcScheduleClientMixin
|
||||
from .source_registry import RpcSourceRegistryClientMixin
|
||||
from .sources import RpcSourceAdminClientMixin
|
||||
|
||||
@@ -21,6 +22,7 @@ class RpcWorkflowApiClient(
|
||||
RpcArtifactClientMixin,
|
||||
RpcDeploymentClientMixin,
|
||||
RpcRunClientMixin,
|
||||
RpcScheduleClientMixin,
|
||||
RpcSourceAdminClientMixin,
|
||||
RpcSourceRegistryClientMixin,
|
||||
RpcAdminClientMixin,
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from wf_api.models import ListSchedulesResult, OccurrencePage, ScheduleResult
|
||||
|
||||
from .base import RpcCaller
|
||||
|
||||
|
||||
class RpcScheduleClientMixin:
|
||||
"""JSON-RPC implementation of workflow schedule administration methods."""
|
||||
|
||||
async def create_schedule(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
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 cast(
|
||||
ScheduleResult,
|
||||
await self._call(
|
||||
"workflow.schedules.create",
|
||||
{
|
||||
"schedule_id": schedule_id,
|
||||
"deployment_id": deployment_id,
|
||||
"trigger": trigger,
|
||||
"input_bindings": (
|
||||
list(input_bindings) if input_bindings is not None else []
|
||||
),
|
||||
"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: RpcCaller, *, schedule_id: str) -> ScheduleResult:
|
||||
return cast(
|
||||
ScheduleResult,
|
||||
await self._call(
|
||||
"workflow.schedules.get",
|
||||
{"schedule_id": schedule_id},
|
||||
),
|
||||
)
|
||||
|
||||
async def list_schedules(
|
||||
self: RpcCaller, *, include_deleted: bool = False
|
||||
) -> ListSchedulesResult:
|
||||
return cast(
|
||||
ListSchedulesResult,
|
||||
await self._call(
|
||||
"workflow.schedules.list",
|
||||
{"include_deleted": include_deleted},
|
||||
),
|
||||
)
|
||||
|
||||
async def update_schedule(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
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 cast(
|
||||
ScheduleResult,
|
||||
await self._call(
|
||||
"workflow.schedules.update",
|
||||
{
|
||||
"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: RpcCaller, *, schedule_id: str) -> ScheduleResult:
|
||||
return cast(
|
||||
ScheduleResult,
|
||||
await self._call(
|
||||
"workflow.schedules.pause",
|
||||
{"schedule_id": schedule_id},
|
||||
),
|
||||
)
|
||||
|
||||
async def resume_schedule(self: RpcCaller, *, schedule_id: str) -> ScheduleResult:
|
||||
return cast(
|
||||
ScheduleResult,
|
||||
await self._call(
|
||||
"workflow.schedules.resume",
|
||||
{"schedule_id": schedule_id},
|
||||
),
|
||||
)
|
||||
|
||||
async def delete_schedule(self: RpcCaller, *, schedule_id: str) -> ScheduleResult:
|
||||
return cast(
|
||||
ScheduleResult,
|
||||
await self._call(
|
||||
"workflow.schedules.delete",
|
||||
{"schedule_id": schedule_id},
|
||||
),
|
||||
)
|
||||
|
||||
async def list_schedule_occurrences(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
schedule_id: str,
|
||||
cursor: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> OccurrencePage:
|
||||
return cast(
|
||||
OccurrencePage,
|
||||
await self._call(
|
||||
"workflow.schedules.occurrences.list",
|
||||
{
|
||||
"schedule_id": schedule_id,
|
||||
"cursor": cursor,
|
||||
"limit": limit,
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Schedule JSON-RPC method registration.
|
||||
|
||||
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
|
||||
|
||||
from wf_api.models import ListSchedulesResult, OccurrencePage, ScheduleResult
|
||||
from wf_server import WorkflowServer
|
||||
|
||||
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||
from ..models import (
|
||||
CreateScheduleParams,
|
||||
DeleteScheduleParams,
|
||||
GetScheduleParams,
|
||||
ListOccurrencesParams,
|
||||
ListSchedulesParams,
|
||||
PauseScheduleParams,
|
||||
ResumeScheduleParams,
|
||||
UpdateScheduleParams,
|
||||
)
|
||||
from ..params import RpcParams
|
||||
|
||||
|
||||
def register_methods(
|
||||
entrypoint: jsonrpc.Entrypoint,
|
||||
server: WorkflowServer,
|
||||
) -> None:
|
||||
"""Register schedule administration JSON-RPC methods.
|
||||
|
||||
No composition-time gate is needed here (unlike drafts): a store-less
|
||||
server raises ``KeyError`` at call time, which maps to a workflow RPC
|
||||
error exactly like any other unknown-schedule failure.
|
||||
"""
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.create", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_create(
|
||||
params: CreateScheduleParams = RpcParams(),
|
||||
) -> ScheduleResult:
|
||||
try:
|
||||
return await server.api.create_schedule(
|
||||
schedule_id=params.schedule_id,
|
||||
deployment_id=params.deployment_id,
|
||||
trigger=params.trigger,
|
||||
input_bindings=params.input_bindings,
|
||||
overlap=params.overlap,
|
||||
misfire=params.misfire,
|
||||
max_active_runs=params.max_active_runs,
|
||||
lateness_allowance_s=params.lateness_allowance_s,
|
||||
max_steps=params.max_steps,
|
||||
enabled=params.enabled,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.get", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_get(
|
||||
params: GetScheduleParams = RpcParams(),
|
||||
) -> ScheduleResult:
|
||||
try:
|
||||
return await server.api.get_schedule(schedule_id=params.schedule_id)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.list", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_list(
|
||||
params: ListSchedulesParams = RpcParams(),
|
||||
) -> ListSchedulesResult:
|
||||
try:
|
||||
return await server.api.list_schedules(
|
||||
include_deleted=params.include_deleted,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.update", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_update(
|
||||
params: UpdateScheduleParams = RpcParams(),
|
||||
) -> ScheduleResult:
|
||||
try:
|
||||
return await server.api.update_schedule(
|
||||
schedule_id=params.schedule_id,
|
||||
expected_revision=params.expected_revision,
|
||||
deployment_id=params.deployment_id,
|
||||
trigger=params.trigger,
|
||||
input_bindings=params.input_bindings,
|
||||
overlap=params.overlap,
|
||||
misfire=params.misfire,
|
||||
max_active_runs=params.max_active_runs,
|
||||
lateness_allowance_s=params.lateness_allowance_s,
|
||||
max_steps=params.max_steps,
|
||||
enabled=params.enabled,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.pause", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_pause(
|
||||
params: PauseScheduleParams = RpcParams(),
|
||||
) -> ScheduleResult:
|
||||
try:
|
||||
return await server.api.pause_schedule(schedule_id=params.schedule_id)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.resume", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_resume(
|
||||
params: ResumeScheduleParams = RpcParams(),
|
||||
) -> ScheduleResult:
|
||||
try:
|
||||
return await server.api.resume_schedule(schedule_id=params.schedule_id)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(name="workflow.schedules.delete", errors=[WorkflowRpcError])
|
||||
async def workflow_schedules_delete(
|
||||
params: DeleteScheduleParams = RpcParams(),
|
||||
) -> ScheduleResult:
|
||||
try:
|
||||
return await server.api.delete_schedule(schedule_id=params.schedule_id)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.schedules.occurrences.list", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_schedules_occurrences_list(
|
||||
params: ListOccurrencesParams = RpcParams(),
|
||||
) -> OccurrencePage:
|
||||
try:
|
||||
return await server.api.list_schedule_occurrences(
|
||||
schedule_id=params.schedule_id,
|
||||
cursor=params.cursor,
|
||||
limit=params.limit,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
@@ -462,6 +462,66 @@ class ResumeRunParams(RpcParamsModel):
|
||||
trace_range: TraceRangeParams | None = None
|
||||
|
||||
|
||||
class CreateScheduleParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
deployment_id: str = Field(min_length=1)
|
||||
trigger: dict[str, Any]
|
||||
input_bindings: list[dict[str, Any]] = Field(default_factory=list)
|
||||
overlap: str = "skip"
|
||||
misfire: str = "skip"
|
||||
max_active_runs: int = Field(default=1, ge=1)
|
||||
lateness_allowance_s: float = Field(default=60.0, ge=0)
|
||||
max_steps: int | None = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
strict=True,
|
||||
description=(
|
||||
"Optional schedule step budget. The server default applies when omitted."
|
||||
),
|
||||
)
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class GetScheduleParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ListSchedulesParams(RpcParamsModel):
|
||||
include_deleted: bool = False
|
||||
|
||||
|
||||
class UpdateScheduleParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
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
|
||||
|
||||
|
||||
class PauseScheduleParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ResumeScheduleParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class DeleteScheduleParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ListOccurrencesParams(RpcParamsModel):
|
||||
schedule_id: str = Field(min_length=1)
|
||||
cursor: str | None = None
|
||||
limit: int = Field(default=50, ge=1, le=100)
|
||||
|
||||
|
||||
class ListRegistryEntriesParams(RpcParamsModel):
|
||||
cursor: str | None = Field(default=None)
|
||||
limit: int = Field(default=50, ge=1, le=100)
|
||||
|
||||
Reference in New Issue
Block a user