diff --git a/temporalio/client.py b/temporalio/client.py index c7e24077..2b52eb3c 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -754,9 +754,6 @@ async def create_schedule( ) -> ScheduleHandle: """Create a schedule and return its handle. - .. warning:: - Schedules are an experimental feature. - Args: id: Unique identifier of the schedule. schedule: Schedule to create. @@ -794,11 +791,7 @@ async def create_schedule( ) def get_schedule_handle(self, id: str) -> ScheduleHandle: - """Get a schedule handle for the given ID. - - .. warning:: - Schedules are an experimental feature. - """ + """Get a schedule handle for the given ID.""" return ScheduleHandle(self, id) async def list_schedules( @@ -817,9 +810,6 @@ async def list_schedules( Note, this list is eventually consistent. Therefore if a schedule is added or deleted, it may not be available in the list immediately. - .. warning:: - Schedules are an experimental feature. - Args: page_size: Maximum number of results for each page. next_page_token: A previously obtained next page token if doing @@ -2101,9 +2091,6 @@ class ScheduleHandle: This is usually created via :py:meth:`Client.get_schedule_handle` or returned from :py:meth:`Client.create_schedule`. - .. warning:: - Schedules are an experimental feature. - Attributes: id: ID of the schedule. """ @@ -2317,9 +2304,6 @@ class ScheduleSpec: The times are the union of :py:attr:`calendars`, :py:attr:`intervals`, and :py:attr:`cron_expressions` excluding anything in :py:attr:`skip`. - - .. warning:: - Schedules are an experimental feature. """ calendars: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) @@ -2405,11 +2389,7 @@ def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleSpec: @dataclass(frozen=True) class ScheduleRange: - """Inclusive range for a schedule match value. - - .. warning:: - Schedules are an experimental feature. - """ + """Inclusive range for a schedule match value.""" start: int """Inclusive start of the range.""" @@ -2456,9 +2436,6 @@ class ScheduleCalendarSpec: A timestamp matches if at least one range of each field matches except for year. If year is missing, that means all years match. For all fields besides year, at least one range must be present to match anything. - - .. warning:: - Schedules are an experimental feature. """ second: Sequence[ScheduleRange] = (ScheduleRange(0),) @@ -2519,9 +2496,6 @@ class ScheduleIntervalSpec: """Specification for scheduling on an interval. Matches times expressed as epoch + (n * every) + offset. - - .. warning:: - Schedules are an experimental feature. """ every: timedelta @@ -2554,9 +2528,6 @@ class ScheduleAction(ABC): See :py:class:`ScheduleActionStartWorkflow` for the most commonly used implementation. - - .. warning:: - Schedules are an experimental feature. """ @staticmethod @@ -2577,11 +2548,7 @@ async def _to_proto( @dataclass class ScheduleActionStartWorkflow(ScheduleAction): - """Schedule action to start a workflow. - - .. warning:: - Schedules are an experimental feature. - """ + """Schedule action to start a workflow.""" workflow: str args: Union[Sequence[Any], Sequence[temporalio.api.common.v1.Payload]] @@ -2826,9 +2793,6 @@ async def _to_proto( class ScheduleOverlapPolicy(IntEnum): """Controls what happens when a workflow would be started by a schedule but one is already running. - - .. warning:: - Schedules are an experimental feature. """ SKIP = int( @@ -2882,9 +2846,6 @@ class ScheduleOverlapPolicy(IntEnum): class ScheduleBackfill: """Time period and policy for actions taken as if the time passed right now. - - .. warning:: - Schedules are an experimental feature. """ start_at: datetime @@ -2916,11 +2877,7 @@ def _to_proto(self) -> temporalio.api.schedule.v1.BackfillRequest: @dataclass class SchedulePolicy: - """Policies of a schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Policies of a schedule.""" overlap: ScheduleOverlapPolicy = dataclasses.field( default_factory=lambda: ScheduleOverlapPolicy.SKIP @@ -2961,11 +2918,7 @@ def _to_proto(self) -> temporalio.api.schedule.v1.SchedulePolicies: @dataclass class ScheduleState: - """State of a schedule - - .. warning:: - Schedules are an experimental feature. - """ + """State of a schedule.""" note: Optional[str] = None """Human readable message for the schedule. @@ -2982,7 +2935,8 @@ class ScheduleState: """ If true, remaining actions will be decremented for each action taken. - Cannot be set on create. + On schedule create, this must be set to true if :py:attr:`remaining_actions` + is non-zero and left false if :py:attr:`remaining_actions` is zero. """ remaining_actions: int = 0 @@ -3011,11 +2965,7 @@ def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleState: @dataclass class Schedule: - """A schedule for periodically running an action. - - .. warning:: - Schedules are an experimental feature. - """ + """A schedule for periodically running an action.""" action: ScheduleAction """Action taken when scheduled.""" @@ -3051,11 +3001,7 @@ async def _to_proto(self, client: Client) -> temporalio.api.schedule.v1.Schedule @dataclass class ScheduleDescription: - """Description of a schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Description of a schedule.""" id: str """ID of the schedule.""" @@ -3160,11 +3106,7 @@ async def memo_value( @dataclass class ScheduleInfo: - """Information about a schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Information about a schedule.""" num_actions: int """Number of actions taken by this schedule.""" @@ -3216,22 +3158,14 @@ def _from_proto(info: temporalio.api.schedule.v1.ScheduleInfo) -> ScheduleInfo: class ScheduleActionExecution(ABC): - """Base class for an action execution. - - .. warning:: - Schedules are an experimental feature. - """ + """Base class for an action execution.""" pass @dataclass class ScheduleActionExecutionStartWorkflow(ScheduleActionExecution): - """Execution of a scheduled workflow start. - - .. warning:: - Schedules are an experimental feature. - """ + """Execution of a scheduled workflow start.""" workflow_id: str """Workflow ID.""" @@ -3251,11 +3185,7 @@ def _from_proto( @dataclass class ScheduleActionResult: - """Information about when an action took place. - - .. warning:: - Schedules are an experimental feature. - """ + """Information about when an action took place.""" scheduled_at: datetime """Scheduled time of the action including jitter.""" @@ -3281,11 +3211,7 @@ def _from_proto( @dataclass class ScheduleUpdateInput: - """Parameter for an update callback for :py:meth:`ScheduleHandle.update`. - - .. warning:: - Schedules are an experimental feature. - """ + """Parameter for an update callback for :py:meth:`ScheduleHandle.update`.""" description: ScheduleDescription """Current description of the schedule.""" @@ -3293,11 +3219,7 @@ class ScheduleUpdateInput: @dataclass class ScheduleUpdate: - """Result of an update callback for :py:meth:`ScheduleHandle.update`. - - .. warning:: - Schedules are an experimental feature. - """ + """Result of an update callback for :py:meth:`ScheduleHandle.update`.""" schedule: Schedule """Schedule to update.""" @@ -3305,11 +3227,7 @@ class ScheduleUpdate: @dataclass class ScheduleListDescription: - """Description of a listed schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Description of a listed schedule.""" id: str """ID of the schedule.""" @@ -3425,11 +3343,7 @@ async def memo_value( @dataclass class ScheduleListSchedule: - """Details for a listed schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Details for a listed schedule.""" action: ScheduleListAction """Action taken when scheduled.""" @@ -3455,22 +3369,14 @@ def _from_proto( class ScheduleListAction(ABC): - """Base class for an action a listed schedule can take. - - .. warning:: - Schedules are an experimental feature. - """ + """Base class for an action a listed schedule can take.""" pass @dataclass class ScheduleListActionStartWorkflow(ScheduleListAction): - """Action to start a workflow on a listed schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Action to start a workflow on a listed schedule.""" workflow: str """Workflow type name.""" @@ -3478,11 +3384,7 @@ class ScheduleListActionStartWorkflow(ScheduleListAction): @dataclass class ScheduleListInfo: - """Information about a listed schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """Information about a listed schedule.""" recent_actions: Sequence[ScheduleActionResult] """Most recent actions, oldest first. @@ -3515,11 +3417,7 @@ def _from_proto( @dataclass class ScheduleListState: - """State of a listed schedule. - - .. warning:: - Schedules are an experimental feature. - """ + """State of a listed schedule.""" note: Optional[str] """Human readable message for the schedule. @@ -3699,11 +3597,7 @@ def __init__(self) -> None: class ScheduleAlreadyRunningError(temporalio.exceptions.TemporalError): - """Error when a schedule is already running. - - .. warning:: - Schedules are an experimental feature. - """ + """Error when a schedule is already running.""" def __init__(self) -> None: """Create schedule already running error.""" @@ -3867,11 +3761,7 @@ class ReportCancellationAsyncActivityInput: @dataclass class CreateScheduleInput: - """Input for :py:meth:`OutboundInterceptor.create_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.create_schedule`.""" id: str schedule: Schedule @@ -3885,11 +3775,7 @@ class CreateScheduleInput: @dataclass class ListSchedulesInput: - """Input for :py:meth:`OutboundInterceptor.list_schedules`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.list_schedules`.""" page_size: int next_page_token: Optional[bytes] @@ -3899,11 +3785,7 @@ class ListSchedulesInput: @dataclass class BackfillScheduleInput: - """Input for :py:meth:`OutboundInterceptor.backfill_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.backfill_schedule`.""" id: str backfills: Sequence[ScheduleBackfill] @@ -3913,11 +3795,7 @@ class BackfillScheduleInput: @dataclass class DeleteScheduleInput: - """Input for :py:meth:`OutboundInterceptor.delete_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.delete_schedule`.""" id: str rpc_metadata: Mapping[str, str] @@ -3926,11 +3804,7 @@ class DeleteScheduleInput: @dataclass class DescribeScheduleInput: - """Input for :py:meth:`OutboundInterceptor.describe_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.describe_schedule`.""" id: str rpc_metadata: Mapping[str, str] @@ -3939,11 +3813,7 @@ class DescribeScheduleInput: @dataclass class PauseScheduleInput: - """Input for :py:meth:`OutboundInterceptor.pause_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.pause_schedule`.""" id: str note: Optional[str] @@ -3953,11 +3823,7 @@ class PauseScheduleInput: @dataclass class TriggerScheduleInput: - """Input for :py:meth:`OutboundInterceptor.trigger_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.trigger_schedule`.""" id: str overlap: Optional[ScheduleOverlapPolicy] @@ -3967,11 +3833,7 @@ class TriggerScheduleInput: @dataclass class UnpauseScheduleInput: - """Input for :py:meth:`OutboundInterceptor.unpause_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.unpause_schedule`.""" id: str note: Optional[str] @@ -3981,11 +3843,7 @@ class UnpauseScheduleInput: @dataclass class UpdateScheduleInput: - """Input for :py:meth:`OutboundInterceptor.update_schedule`. - - .. warning:: - Schedules are an experimental feature. - """ + """Input for :py:meth:`OutboundInterceptor.update_schedule`.""" id: str updater: Callable[ @@ -4528,6 +4386,23 @@ async def report_cancellation_async_activity( ### Schedule calls async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: + # Limited actions must be false if remaining actions is 0 and must be + # true if remaining actions is non-zero + if ( + input.schedule.state.limited_actions + and not input.schedule.state.remaining_actions + ): + raise ValueError( + "Must set limited actions to false if there are no remaining actions set" + ) + if ( + not input.schedule.state.limited_actions + and input.schedule.state.remaining_actions + ): + raise ValueError( + "Must set limited actions to true if there are remaining actions set" + ) + initial_patch: Optional[temporalio.api.schedule.v1.SchedulePatch] = None if input.trigger_immediately or input.backfill: initial_patch = temporalio.api.schedule.v1.SchedulePatch( diff --git a/tests/test_client.py b/tests/test_client.py index cb23d8ea..6f523714 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -653,7 +653,9 @@ async def test_schedule_basics( catchup_window=timedelta(minutes=5), pause_on_failure=True, ), - state=ScheduleState(note="sched note 1", paused=True, remaining_actions=30), + state=ScheduleState( + note="sched note 1", paused=True, limited_actions=True, remaining_actions=30 + ), ) handle = await client.create_schedule( f"schedule-{uuid.uuid4()}", @@ -953,6 +955,29 @@ async def test_schedule_backfill( await assert_no_schedules(client) +async def test_schedule_create_limited_actions_validation( + client: Client, worker: ExternalWorker, env: WorkflowEnvironment +): + sched = Schedule( + action=ScheduleActionStartWorkflow( + "some workflow", + [], + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ), + spec=ScheduleSpec(), + ) + with pytest.raises(ValueError) as err: + sched.state.limited_actions = True + await client.create_schedule(f"schedule-{uuid.uuid4()}", sched) + assert "are no remaining actions set" in str(err.value) + with pytest.raises(ValueError) as err: + sched.state.limited_actions = False + sched.state.remaining_actions = 10 + await client.create_schedule(f"schedule-{uuid.uuid4()}", sched) + assert "are remaining actions set" in str(err.value) + + async def assert_no_schedules(client: Client) -> None: # Listing appears eventually consistent async def schedule_count() -> int: diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index fdf66130..fe35b45d 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -2042,6 +2042,11 @@ async def run(self) -> None: async def test_workflow_patch(client: Client): + # TODO(cretz): Patches have issues on older servers since core needs patch + # metadata support for some fixes. Unskip for local server only once we + # upgrade to https://github.com/temporalio/sdk-python/issues/272. + pytest.skip("Needs SDK metadata support") + workflow_run = PrePatchWorkflow.run task_queue = str(uuid.uuid4())