diff --git a/temporalio/client.py b/temporalio/client.py deleted file mode 100644 index ff7ae4314..000000000 --- a/temporalio/client.py +++ /dev/null @@ -1,10011 +0,0 @@ -"""Client for accessing Temporal.""" - -from __future__ import annotations - -import abc -import asyncio -import copy -import dataclasses -import functools -import inspect -import json -import re -import uuid -import warnings -from abc import ABC, abstractmethod -from asyncio import Future -from collections.abc import ( - AsyncIterator, - Awaitable, - Callable, - Iterable, - Mapping, - Sequence, -) -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from enum import Enum, IntEnum -from typing import ( - Any, - Concatenate, - Generic, - cast, - overload, -) - -import google.protobuf.duration_pb2 -import google.protobuf.json_format -import google.protobuf.timestamp_pb2 -from google.protobuf.internal.containers import MessageMap -from typing_extensions import Required, Self, TypedDict - -import temporalio.activity -import temporalio.api.activity.v1 -import temporalio.api.common.v1 -import temporalio.api.enums.v1 -import temporalio.api.errordetails.v1 -import temporalio.api.failure.v1 -import temporalio.api.history.v1 -import temporalio.api.schedule.v1 -import temporalio.api.sdk.v1 -import temporalio.api.taskqueue.v1 -import temporalio.api.update.v1 -import temporalio.api.workflow.v1 -import temporalio.api.workflowservice.v1 -import temporalio.common -import temporalio.converter -import temporalio.converter._search_attributes -import temporalio.exceptions -import temporalio.nexus -import temporalio.nexus._operation_context -import temporalio.runtime -import temporalio.service -import temporalio.workflow -from temporalio.activity import ActivityCancellationDetails -from temporalio.converter import ( - ActivitySerializationContext, - DataConverter, - SerializationContext, - StorageDriverActivityInfo, - StorageDriverStoreContext, - StorageDriverWorkflowInfo, - WithSerializationContext, - WorkflowSerializationContext, -) -from temporalio.service import ( - ConnectConfig, - DnsLoadBalancingConfig, - HttpConnectProxyConfig, - KeepAliveConfig, - RetryConfig, - RPCError, - RPCStatusCode, - ServiceClient, - TLSConfig, -) - -from .common import HeaderCodecBehavior -from .types import ( - AnyType, - CallableAsyncNoParam, - CallableAsyncSingleParam, - CallableSyncNoParam, - CallableSyncSingleParam, - LocalReturnType, - MethodAsyncNoParam, - MethodAsyncSingleParam, - MethodSyncOrAsyncNoParam, - MethodSyncOrAsyncSingleParam, - MultiParamSpec, - ParamType, - ReturnType, - SelfType, -) - - -class Client: - """Client for accessing Temporal. - - Most users will use :py:meth:`connect` to create a client. The - :py:attr:`service` property provides access to a raw gRPC client. To create - another client, like for a different namespace, :py:func:`Client` may be - directly instantiated with a :py:attr:`service` of another. - - Clients are not thread-safe and should only be used in the event loop they - are first connected in. If a client needs to be used from another thread - than where it was created, make sure the event loop where it was created is - captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the - client call and that event loop. - - Clients do not work across forks since runtimes do not work across forks. - """ - - @classmethod - async def connect( - cls, - target_host: str, - *, - namespace: str = "default", - api_key: str | None = None, - data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, - plugins: Sequence[Plugin] = [], - interceptors: Sequence[Interceptor] = [], - default_workflow_query_reject_condition: None - | (temporalio.common.QueryRejectCondition) = None, - tls: bool | TLSConfig | None = None, - retry_config: RetryConfig | None = None, - keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default, - rpc_metadata: Mapping[str, str | bytes] = {}, - identity: str | None = None, - lazy: bool = False, - runtime: temporalio.runtime.Runtime | None = None, - http_connect_proxy_config: HttpConnectProxyConfig | None = None, - dns_load_balancing_config: DnsLoadBalancingConfig | None = None, - header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, - ) -> Self: - """Connect to a Temporal server. - - Args: - target_host: ``host:port`` for the Temporal server. For local - development, this is often "localhost:7233". - namespace: Namespace to use for client calls. - api_key: API key for Temporal. This becomes the "Authorization" - HTTP header with "Bearer " prepended. This is only set if RPC - metadata doesn't already have an "authorization" key. - data_converter: Data converter to use for all data conversions - to/from payloads. - plugins: Set of plugins that are chained together to allow - intercepting and modifying client creation and service connection. - The earlier plugins wrap the later ones. - - Any plugins that also implement - :py:class:`temporalio.worker.Plugin` will be used as worker - plugins too so they should not be given when creating a - worker. - interceptors: Set of interceptors that are chained together to allow - intercepting of client calls. The earlier interceptors wrap the - later ones. - - Any interceptors that also implement - :py:class:`temporalio.worker.Interceptor` will be used as worker - interceptors too so they should not be given when creating a - worker. - default_workflow_query_reject_condition: The default rejection - condition for workflow queries if not set during query. See - :py:meth:`WorkflowHandle.query` for details on the rejection - condition. - tls: If ``None``, the default, TLS will be enabled automatically - when ``api_key`` is provided, otherwise TLS is disabled. If - ``False``, do not use TLS. If ``True``, use system default TLS - configuration. If TLS configuration present, that TLS - configuration will be used. - retry_config: Retry configuration for direct service calls (when - opted in) or all high-level calls made by this client (which all - opt-in to retries by default). If unset, a default retry - configuration is used. - keep_alive_config: Keep-alive configuration for the client - connection. Default is to check every 30s and kill the - connection if a response doesn't come back in 15s. Can be set to - ``None`` to disable. - rpc_metadata: Headers to use for all calls to the server. Keys here - can be overriden by per-call RPC metadata keys. - identity: Identity for this client. If unset, a default is created - based on the version of the SDK. - lazy: If true, the client will not connect until the first call is - attempted or a worker is created with it. Lazy clients cannot be - used for workers. - runtime: The runtime for this client, or the default if unset. - http_connect_proxy_config: Configuration for HTTP CONNECT proxy. - dns_load_balancing_config: DNS load balancing configuration for the - client connection. Default is to re-resolve DNS every 30s. Can - be set to ``None`` to disable. Silently disabled when - ``http_connect_proxy_config`` is set, since the two are mutually - exclusive. - header_codec_behavior: Encoding behavior for headers sent by the client. - """ - connect_config = temporalio.service.ConnectConfig( - target_host=target_host, - api_key=api_key, - tls=tls, - retry_config=retry_config, - keep_alive_config=keep_alive_config, - rpc_metadata=rpc_metadata, - identity=identity or "", - lazy=lazy, - runtime=runtime, - http_connect_proxy_config=http_connect_proxy_config, - dns_load_balancing_config=dns_load_balancing_config, - ) - - def make_lambda( - plugin: Plugin, next: Callable[[ConnectConfig], Awaitable[ServiceClient]] - ): - return lambda config: plugin.connect_service_client(config, next) - - next_function = ServiceClient.connect - for plugin in reversed(plugins): - next_function = make_lambda(plugin, next_function) - - service_client = await next_function(connect_config) - - return cls( - service_client, - namespace=namespace, - data_converter=data_converter, - interceptors=interceptors, - default_workflow_query_reject_condition=default_workflow_query_reject_condition, - header_codec_behavior=header_codec_behavior, - plugins=plugins, - ) - - def __init__( - self, - service_client: temporalio.service.ServiceClient, - *, - namespace: str = "default", - data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, - plugins: Sequence[Plugin] = [], - interceptors: Sequence[Interceptor] = [], - default_workflow_query_reject_condition: None - | (temporalio.common.QueryRejectCondition) = None, - header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, - ): - """Create a Temporal client from a service client. - - See :py:meth:`connect` for details on the parameters. - """ - # Store the config for tracking - config = ClientConfig( - service_client=service_client, - namespace=namespace, - data_converter=data_converter, - plugins=plugins, - interceptors=interceptors, - default_workflow_query_reject_condition=default_workflow_query_reject_condition, - header_codec_behavior=header_codec_behavior, - ) - self._initial_config = config.copy() - - for plugin in plugins: - config = plugin.configure_client(config) - - self._init_from_config(config) - - def _init_from_config(self, config: ClientConfig): - self._config = config - - # Iterate over interceptors in reverse building the impl - self._impl: OutboundInterceptor = _ClientImpl(self) - for interceptor in reversed(list(self._config["interceptors"])): - self._impl = interceptor.intercept_client(self._impl) - - def config(self, *, active_config: bool = False) -> ClientConfig: - """Config, as a dictionary, used to create this client. - - Args: - active_config: If true, return the modified configuration in use rather than the initial one - provided to the client. - - This makes a shallow copy of the config each call. - """ - config = self._config.copy() if active_config else self._initial_config.copy() - config["interceptors"] = list(config["interceptors"]) - return config - - @property - def service_client(self) -> temporalio.service.ServiceClient: - """Raw gRPC service client.""" - return self._config["service_client"] - - @property - def workflow_service(self) -> temporalio.service.WorkflowService: - """Raw gRPC workflow service client.""" - return self._config["service_client"].workflow_service - - @property - def operator_service(self) -> temporalio.service.OperatorService: - """Raw gRPC operator service client.""" - return self._config["service_client"].operator_service - - @property - def test_service(self) -> temporalio.service.TestService: - """Raw gRPC test service client.""" - return self._config["service_client"].test_service - - @property - def namespace(self) -> str: - """Namespace used in calls by this client.""" - return self._config["namespace"] - - @property - def identity(self) -> str: - """Identity used in calls by this client.""" - return self._config["service_client"].config.identity - - @property - def data_converter(self) -> temporalio.converter.DataConverter: - """Data converter used by this client.""" - return self._config["data_converter"] - - @property - def rpc_metadata(self) -> Mapping[str, str | bytes]: - """Headers for every call made by this client. - - Do not use mutate this mapping. Rather, set this property with an - entirely new mapping to change the headers. - """ - return self.service_client.config.rpc_metadata - - @rpc_metadata.setter - def rpc_metadata(self, value: Mapping[str, str | bytes]) -> None: - """Update the headers for this client. - - Do not mutate this mapping after set. Rather, set an entirely new - mapping if changes are needed. - - Raises: - TypeError: the key/value pair is not a valid gRPC ASCII or binary metadata. - All binary metadata must be supplied as bytes, and the key must end in '-bin'. - - .. warning:: - Attempting to set an invalid binary RPC metadata value may leave the client - in an inconsistent state (as well as raise a :py:class:`TypeError`). - """ - # Update config and perform update - # This may raise if the metadata is invalid: - self.service_client.update_rpc_metadata(value) - self.service_client.config.rpc_metadata = value - - @property - def api_key(self) -> str | None: - """API key for every call made by this client.""" - return self.service_client.config.api_key - - @api_key.setter - def api_key(self, value: str | None) -> None: - """Update the API key for this client. - - This is only set if RPCmetadata doesn't already have an "authorization" - key. - """ - # Update config and perform update - self.service_client.config.api_key = value - self.service_client.update_api_key(value) - - # Overload for no-param workflow - @overload - async def start_workflow( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> WorkflowHandle[SelfType, ReturnType]: ... - - # Overload for single-param workflow - @overload - async def start_workflow( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> WorkflowHandle[SelfType, ReturnType]: ... - - # Overload for multi-param workflow - @overload - async def start_workflow( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> WorkflowHandle[SelfType, ReturnType]: ... - - # Overload for string-name workflow - @overload - async def start_workflow( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> WorkflowHandle[Any, Any]: ... - - async def start_workflow( - self, - workflow: str | Callable[..., Awaitable[Any]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - # The following options should not be considered part of the public API. They - # are deliberately not exposed in overloads, and are not subject to any - # backwards compatibility guarantees. - callbacks: Sequence[Callback] = [], - workflow_event_links: Sequence[ - temporalio.api.common.v1.Link.WorkflowEvent - ] = [], - request_id: str | None = None, - stack_level: int = 2, - ) -> WorkflowHandle[Any, Any]: - """Start a workflow and return its handle. - - Args: - workflow: String name or class method decorated with - ``@workflow.run`` for the workflow to start. - arg: Single argument to the workflow. - args: Multiple arguments to the workflow. Cannot be set if arg is. - id: Unique identifier for the workflow execution. - task_queue: Task queue to run the workflow on. - result_type: For string workflows, this can set the specific result - type hint to deserialize into. - execution_timeout: Total workflow execution timeout including - retries and continue as new. - run_timeout: Timeout of a single workflow run. - task_timeout: Timeout of a single workflow task. - id_conflict_policy: Behavior when a workflow is currently running with the same ID. - Default is UNSPECIFIED, which effectively means fail the start attempt. - Set to USE_EXISTING for idempotent deduplication on workflow ID. - Cannot be set if ``id_reuse_policy`` is set to TERMINATE_IF_RUNNING. - id_reuse_policy: Behavior when a closed workflow with the same ID exists. - Default is ALLOW_DUPLICATE. - retry_policy: Retry policy for the workflow. - cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ - memo: Memo for the workflow. - search_attributes: Search attributes for the workflow. The - dictionary form of this is deprecated, use - :py:class:`temporalio.common.TypedSearchAttributes`. - static_summary: A single-line fixed summary for this workflow execution that may appear - in the UI/CLI. This can be in single-line Temporal markdown format. - static_details: General fixed details for this workflow execution that may appear in - UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is - a fixed value on the workflow that cannot be updated. For details that can be - updated, use :py:meth:`temporalio.workflow.get_current_details` within the workflow. - start_delay: Amount of time to wait before starting the workflow. - This does not work with ``cron_schedule``. - start_signal: If present, this signal is sent as signal-with-start - instead of traditional workflow start. - start_signal_args: Arguments for start_signal if start_signal - present. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - request_eager_start: Potentially reduce the latency to start this workflow by - encouraging the server to start it on a local worker running with - this same client. - priority: Priority of the workflow execution. - versioning_override: Overrides the versioning behavior for this workflow. - - Returns: - A workflow handle to the started workflow. - - Raises: - temporalio.exceptions.WorkflowAlreadyStartedError: Workflow has - already been started. - RPCError: Workflow could not be started for some other reason. - """ - temporalio.common._warn_on_deprecated_search_attributes( - search_attributes, stack_level=stack_level - ) - name, result_type_from_type_hint = ( - temporalio.workflow._Definition.get_name_and_result_type(workflow) - ) - return await self._impl.start_workflow( - StartWorkflowInput( - workflow=name, - args=temporalio.common._arg_or_args(arg, args), - id=id, - task_queue=task_queue, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - start_delay=start_delay, - versioning_override=versioning_override, - headers={}, - static_summary=static_summary, - static_details=static_details, - start_signal=start_signal, - start_signal_args=start_signal_args, - ret_type=result_type or result_type_from_type_hint, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - request_eager_start=request_eager_start, - priority=priority, - callbacks=callbacks, - workflow_event_links=workflow_event_links, - request_id=request_id, - ) - ) - - # Overload for no-param workflow - @overload - async def execute_workflow( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> ReturnType: ... - - # Overload for single-param workflow - @overload - async def execute_workflow( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> ReturnType: ... - - # Overload for multi-param workflow - @overload - async def execute_workflow( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> ReturnType: ... - - # Overload for string-name workflow - @overload - async def execute_workflow( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> Any: ... - - async def execute_workflow( - self, - workflow: str | Callable[..., Awaitable[Any]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - start_signal: str | None = None, - start_signal_args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - request_eager_start: bool = False, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> Any: - """Start a workflow and wait for completion. - - This is a shortcut for :py:meth:`start_workflow` + - :py:meth:`WorkflowHandle.result`. - """ - return await ( - # We have to tell MyPy to ignore errors here because we want to call - # the non-@overload form of this and MyPy does not support that - await self.start_workflow( # type: ignore - workflow, # type: ignore[arg-type] - arg, - args=args, - task_queue=task_queue, - result_type=result_type, - id=id, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - static_summary=static_summary, - static_details=static_details, - start_delay=start_delay, - start_signal=start_signal, - start_signal_args=start_signal_args, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - request_eager_start=request_eager_start, - priority=priority, - versioning_override=versioning_override, - stack_level=3, - ) - ).result() - - def get_workflow_handle( - self, - workflow_id: str, - *, - run_id: str | None = None, - first_execution_run_id: str | None = None, - result_type: type | None = None, - ) -> WorkflowHandle[Any, Any]: - """Get a workflow handle to an existing workflow by its ID. - - Args: - workflow_id: Workflow ID to get a handle to. - run_id: Run ID that will be used for all calls. - first_execution_run_id: First execution run ID used for cancellation - and termination. - result_type: The result type to deserialize into if known. - - Returns: - The workflow handle. - """ - return WorkflowHandle( - self, - workflow_id, - run_id=run_id, - result_run_id=run_id, - first_execution_run_id=first_execution_run_id, - result_type=result_type, - ) - - def get_workflow_handle_for( - self, - workflow: ( - MethodAsyncNoParam[SelfType, ReturnType] - | MethodAsyncSingleParam[SelfType, Any, ReturnType] - ), - workflow_id: str, - *, - run_id: str | None = None, - first_execution_run_id: str | None = None, - ) -> WorkflowHandle[SelfType, ReturnType]: - """Get a typed workflow handle to an existing workflow by its ID. - - This is the same as :py:meth:`get_workflow_handle` but typed. - - Args: - workflow: The workflow run method to use for typing the handle. - workflow_id: Workflow ID to get a handle to. - run_id: Run ID that will be used for all calls. - first_execution_run_id: First execution run ID used for cancellation - and termination. - - Returns: - The workflow handle. - """ - defn = temporalio.workflow._Definition.must_from_run_fn(workflow) - return self.get_workflow_handle( - workflow_id, - run_id=run_id, - first_execution_run_id=first_execution_run_id, - result_type=defn.ret_type, - ) - - # Overload for no-param update - @overload - async def execute_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for single-param update - @overload - async def execute_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for multi-param update - @overload - async def execute_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # type: ignore - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for string-name update - @overload - async def execute_update_with_start_workflow( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: ... - - async def execute_update_with_start_workflow( - self, - update: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: - """Send an update-with-start request and wait for the update to complete. - - A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the - specified workflow execution is not running, a new workflow execution is started - and the update is sent in the first workflow task. Alternatively if the specified - workflow execution is running then, if the WorkflowIDConflictPolicy is - USE_EXISTING, the update is issued against the specified workflow, and if the - WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until - the update has completed, and return the update result. Note that this means that - the call will not return successfully until the update has been delivered to a - worker. - - Args: - update: Update function or name on the workflow. arg: Single argument to the - update. - args: Multiple arguments to the update. Cannot be set if arg is. - start_workflow_operation: a WithStartWorkflowOperation definining the - WorkflowIDConflictPolicy and how to start the workflow in the event that a - workflow is started. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed out or - cancelled. - - RPCError: There was some issue starting the workflow or sending the update to - the workflow. - """ - handle = await self._start_update_with_start( - update, - arg, - args=args, - start_workflow_operation=start_workflow_operation, - wait_for_stage=WorkflowUpdateStage.COMPLETED, - id=id, - result_type=result_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - return await handle.result() - - # Overload for no-param start update - @overload - async def start_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - wait_for_stage: WorkflowUpdateStage, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for single-param start update - @overload - async def start_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - wait_for_stage: WorkflowUpdateStage, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for multi-param start update - @overload - async def start_update_with_start_workflow( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # type: ignore - start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], - wait_for_stage: WorkflowUpdateStage, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for string-name start update - @overload - async def start_update_with_start_workflow( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[Any]: ... - - async def start_update_with_start_workflow( - self, - update: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - start_workflow_operation: WithStartWorkflowOperation[Any, Any], - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[Any]: - """Send an update-with-start request and wait for it to be accepted. - - A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the - specified workflow execution is not running, a new workflow execution is started - and the update is sent in the first workflow task. Alternatively if the specified - workflow execution is running then, if the WorkflowIDConflictPolicy is - USE_EXISTING, the update is issued against the specified workflow, and if the - WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until - the update has been accepted, and return a WorkflowUpdateHandle. Note that this - means that the call will not return successfully until the update has been - delivered to a worker. - - Args: - update: Update function or name on the workflow. arg: Single argument to the - update. - args: Multiple arguments to the update. Cannot be set if arg is. - start_workflow_operation: a WithStartWorkflowOperation definining the - WorkflowIDConflictPolicy and how to start the workflow in the event that a - workflow is started. - wait_for_stage: Required stage to wait until returning: either ACCEPTED or - COMPLETED. ADMITTED is not currently supported. See - https://docs.temporal.io/workflows#update for more details. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed out or - cancelled. - - RPCError: There was some issue starting the workflow or sending the update to - the workflow. - """ - return await self._start_update_with_start( - update, - arg, - wait_for_stage=wait_for_stage, - args=args, - id=id, - result_type=result_type, - start_workflow_operation=start_workflow_operation, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - async def _start_update_with_start( - self, - update: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - start_workflow_operation: WithStartWorkflowOperation[SelfType, ReturnType], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[Any]: - if wait_for_stage == WorkflowUpdateStage.ADMITTED: - raise ValueError("ADMITTED wait stage not supported") - - if start_workflow_operation._used: - raise RuntimeError("WithStartWorkflowOperation cannot be reused") - start_workflow_operation._used = True - - update_name, result_type_from_type_hint = ( - temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) - ) - - update_input = UpdateWithStartUpdateWorkflowInput( - update_id=id, - update=update_name, - args=temporalio.common._arg_or_args(arg, args), - headers={}, - ret_type=result_type or result_type_from_type_hint, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - wait_for_stage=wait_for_stage, - ) - - def on_start( - start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - ): - start_workflow_operation._workflow_handle.set_result( - WorkflowHandle( - self, - start_workflow_operation._start_workflow_input.id, - first_execution_run_id=start_response.run_id, - result_run_id=start_response.run_id, - result_type=start_workflow_operation._start_workflow_input.ret_type, - ) - ) - - def on_start_error( - error: BaseException, - ): - start_workflow_operation._workflow_handle.set_exception(error) - - input = StartWorkflowUpdateWithStartInput( - start_workflow_input=start_workflow_operation._start_workflow_input, - update_workflow_input=update_input, - _on_start=on_start, - _on_start_error=on_start_error, - ) - - return await self._impl.start_update_with_start_workflow(input) - - def list_workflows( - self, - query: str | None = None, - *, - limit: int | None = None, - page_size: int = 1000, - next_page_token: bytes | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowExecutionAsyncIterator: - """List workflows. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Args: - query: A Temporal visibility list filter. See Temporal documentation - concerning visibility list filters including behavior when left - unset. - limit: Maximum number of workflows to return. If unset, all - workflows are returned. Only applies if using the - returned :py:class:`WorkflowExecutionAsyncIterator`. - as an async iterator. - page_size: Maximum number of results for each page. - next_page_token: A previously obtained next page token if doing - pagination. Usually not needed as the iterator automatically - starts from the beginning. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that can be used with ``async for``. - """ - return self._impl.list_workflows( - ListWorkflowsInput( - query=query, - page_size=page_size, - next_page_token=next_page_token, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - limit=limit, - ) - ) - - async def count_workflows( - self, - query: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowExecutionCount: - """Count workflows. - - Args: - query: A Temporal visibility filter. See Temporal documentation - concerning visibility list filters. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - Count of workflows. - """ - return await self._impl.count_workflows( - CountWorkflowsInput( - query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout - ) - ) - - # async no-param - @overload - async def start_activity( - self, - activity: CallableAsyncNoParam[ReturnType], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync no-param - @overload - async def start_activity( - self, - activity: CallableSyncNoParam[ReturnType], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # async single-param - @overload - async def start_activity( - self, - activity: CallableAsyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync single-param - @overload - async def start_activity( - self, - activity: CallableSyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # async multi-param - @overload - async def start_activity( - self, - activity: Callable[..., Awaitable[ReturnType]], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync multi-param - @overload - async def start_activity( - self, - activity: Callable[..., ReturnType], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # string name - @overload - async def start_activity( - self, - activity: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[Any]: ... - - async def start_activity( - self, - activity: ( - str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] - ), - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - # Either schedule_to_close_timeout or start_to_close_timeout must be present - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: - """Start an activity and return its handle. - - .. warning:: - This API is experimental. - - Args: - activity: String name or callable activity function to execute. - arg: Single argument to the activity. - args: Multiple arguments to the activity. Cannot be set if arg is. - id: Unique identifier for the activity. Required. - task_queue: Task queue to send the activity to. - result_type: For string name activities, optional type to deserialize result into. - schedule_to_close_timeout: Total time allowed for the activity from schedule to completion. - schedule_to_start_timeout: Time allowed for the activity to sit in the task queue. - start_to_close_timeout: Time allowed for a single execution attempt. - heartbeat_timeout: Time between heartbeats before the activity is considered failed. - id_reuse_policy: How to handle reusing activity IDs from closed activities. - Default is ALLOW_DUPLICATE. - id_conflict_policy: How to handle activity ID conflicts with running activities. - Default is FAIL. - retry_policy: Retry policy for the activity. - search_attributes: Search attributes for the activity. - summary: A single-line fixed summary for this activity that may appear - in the UI/CLI. This can be in single-line Temporal markdown format. - priority: Priority of the activity execution. - start_delay: Time to wait before dispatching the activity. - This delay is not applied to retry attempts. - rpc_metadata: Headers used on the RPC call. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - A handle to the started activity. - """ - name, result_type_from_type_annotation = ( - temporalio.activity._Definition.get_name_and_result_type(activity) - ) - return await self._impl.start_activity( - StartActivityInput( - activity_type=name, - args=temporalio.common._arg_or_args(arg, args), - id=id, - task_queue=task_queue, - result_type=result_type or result_type_from_type_annotation, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - search_attributes=search_attributes, - summary=summary, - start_delay=start_delay, - headers={}, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - priority=priority, - ) - ) - - # async no-param - @overload - async def execute_activity( - self, - activity: CallableAsyncNoParam[ReturnType], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync no-param - @overload - async def execute_activity( - self, - activity: CallableSyncNoParam[ReturnType], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # async single-param - @overload - async def execute_activity( - self, - activity: CallableAsyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync single-param - @overload - async def execute_activity( - self, - activity: CallableSyncSingleParam[ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # async multi-param - @overload - async def execute_activity( - self, - activity: Callable[..., Awaitable[ReturnType]], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync multi-param - @overload - async def execute_activity( - self, - activity: Callable[..., ReturnType], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # string name - @overload - async def execute_activity( - self, - activity: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: ... - - async def execute_activity( - self, - activity: ( - str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] - ), - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - # Either schedule_to_close_timeout or start_to_close_timeout must be present - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: - """Start an activity, wait for it to complete, and return its result. - - .. warning:: - This API is experimental. - - This is a convenience method that combines :py:meth:`start_activity` and - :py:meth:`ActivityHandle.result`. - - Returns: - The result of the activity. - - Raises: - ActivityFailureError: If the activity completed with a failure. - """ - handle: ActivityHandle[ReturnType] = await self.start_activity( - cast(Any, activity), - arg, - args=args, - id=id, - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - search_attributes=search_attributes, - summary=summary, - priority=priority, - start_delay=start_delay, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - return await handle.result() - - # async no-param - @overload - async def start_activity_class( - self, - activity: type[CallableAsyncNoParam[ReturnType]], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync no-param - @overload - async def start_activity_class( - self, - activity: type[CallableSyncNoParam[ReturnType]], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # async single-param - @overload - async def start_activity_class( - self, - activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync single-param - @overload - async def start_activity_class( - self, - activity: type[CallableSyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # async multi-param - @overload - async def start_activity_class( - self, - activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync multi-param - @overload - async def start_activity_class( - self, - activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - async def start_activity_class( - self, - activity: type[Callable], # type: ignore[reportInvalidTypeForm] - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[Any]: - """Start an activity from a callable class. - - .. warning:: - This API is experimental. - - See :py:meth:`start_activity` for parameter and return details. - """ - return await self.start_activity( - cast(Any, activity), - arg, - args=args, - id=id, - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - search_attributes=search_attributes, - summary=summary, - priority=priority, - start_delay=start_delay, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - # async no-param - @overload - async def execute_activity_class( - self, - activity: type[CallableAsyncNoParam[ReturnType]], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync no-param - @overload - async def execute_activity_class( - self, - activity: type[CallableSyncNoParam[ReturnType]], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # async single-param - @overload - async def execute_activity_class( - self, - activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync single-param - @overload - async def execute_activity_class( - self, - activity: type[CallableSyncSingleParam[ParamType, ReturnType]], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # async multi-param - @overload - async def execute_activity_class( - self, - activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync multi-param - @overload - async def execute_activity_class( - self, - activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - async def execute_activity_class( - self, - activity: type[Callable], # type: ignore[reportInvalidTypeForm] - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: - """Start an activity from a callable class and wait for completion. - - .. warning:: - This API is experimental. - - This is a shortcut for ``await`` :py:meth:`start_activity_class`. - """ - return await self.execute_activity( - cast(Any, activity), - arg, - args=args, - id=id, - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - search_attributes=search_attributes, - summary=summary, - priority=priority, - start_delay=start_delay, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - # async no-param - @overload - async def start_activity_method( - self, - activity: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # async single-param - @overload - async def start_activity_method( - self, - activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # async multi-param - @overload - async def start_activity_method( - self, - activity: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - # sync multi-param - @overload - async def start_activity_method( - self, - activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[ReturnType]: ... - - async def start_activity_method( - self, - activity: Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityHandle[Any]: - """Start an activity from a method. - - .. warning:: - This API is experimental. - - See :py:meth:`start_activity` for parameter and return details. - """ - return await self.start_activity( - cast(Any, activity), - arg, - args=args, - id=id, - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - search_attributes=search_attributes, - summary=summary, - priority=priority, - start_delay=start_delay, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - # async no-param - @overload - async def execute_activity_method( - self, - activity: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # async single-param - @overload - async def execute_activity_method( - self, - activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # async multi-param - @overload - async def execute_activity_method( - self, - activity: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - # sync multi-param - @overload - async def execute_activity_method( - self, - activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], - *, - args: Sequence[Any], - id: str, - task_queue: str, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: ... - - async def execute_activity_method( - self, - activity: Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - result_type: type | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, - retry_policy: temporalio.common.RetryPolicy | None = None, - search_attributes: temporalio.common.TypedSearchAttributes | None = None, - summary: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: - """Start an activity from a method and wait for completion. - - .. warning:: - This API is experimental. - - This is a shortcut for ``await`` :py:meth:`start_activity_method`. - """ - return await self.execute_activity( - cast(Any, activity), - arg, - args=args, - id=id, - task_queue=task_queue, - result_type=result_type, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - search_attributes=search_attributes, - summary=summary, - priority=priority, - start_delay=start_delay, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - def list_activities( - self, - query: str, - *, - limit: int | None = None, - page_size: int = 1000, - next_page_token: bytes | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityExecutionAsyncIterator: - """List activities not started by a workflow. - - .. warning:: - This API is experimental. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Args: - query: A Temporal visibility list filter for activities. Required. - limit: Maximum number of activities to return. If unset, all - activities are returned. Only applies if using the - returned :py:class:`ActivityExecutionAsyncIterator` - as an async iterator. - page_size: Maximum number of results for each page. - next_page_token: A previously obtained next page token if doing - pagination. Usually not needed as the iterator automatically - starts from the beginning. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that can be used with ``async for``. - """ - return self._impl.list_activities( - ListActivitiesInput( - query=query, - page_size=page_size, - next_page_token=next_page_token, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - limit=limit, - ) - ) - - async def count_activities( - self, - query: str | None = None, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityExecutionCount: - """Count activities not started by a workflow. - - .. warning:: - This API is experimental. - - Args: - query: A Temporal visibility filter for activities. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - Count of activities. - """ - return await self._impl.count_activities( - CountActivitiesInput( - query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout - ) - ) - - @overload - def get_activity_handle( - self, - activity_id: str, - *, - run_id: str | None = None, - ) -> ActivityHandle[Any]: ... - - @overload - def get_activity_handle( - self, - activity_id: str, - *, - run_id: str | None = None, - result_type: type[ReturnType], - ) -> ActivityHandle[ReturnType]: ... - - def get_activity_handle( - self, - activity_id: str, - *, - run_id: str | None = None, - result_type: type | None = None, - ) -> ActivityHandle[Any]: - """Get a handle to an existing activity, as the caller of that activity. - - The activity must not have been started by a workflow. - - .. warning:: - This API is experimental. - - To get a handle to an activity execution that you control for manual completion and - heartbeating, see :py:meth:`Client.get_async_activity_handle`. - - Args: - activity_id: The activity ID. - run_id: The activity run ID. If not provided, targets the latest run. - result_type: The result type to deserialize into. - - Returns: - A handle to the activity. - """ - return ActivityHandle( - self, - activity_id, - run_id=run_id, - result_type=result_type, - ) - - @overload - def get_async_activity_handle( - self, *, activity_id: str, run_id: str | None = None - ) -> AsyncActivityHandle: - pass - - @overload - def get_async_activity_handle( - self, *, workflow_id: str, run_id: str | None, activity_id: str - ) -> AsyncActivityHandle: - pass - - @overload - def get_async_activity_handle(self, *, task_token: bytes) -> AsyncActivityHandle: - pass - - def get_async_activity_handle( - self, - *, - workflow_id: str | None = None, - run_id: str | None = None, - activity_id: str | None = None, - task_token: bytes | None = None, - ) -> AsyncActivityHandle: - """Get a handle to an activity execution that you control, for manual - completion and heartbeating. - - To get a handle to an activity execution as the caller of that activity, - see :py:meth:`Client.get_activity_handle`. - - This function may be used to get a handle to an activity started by a - client, or an activity started by a workflow. - - To get a handle to an activity started by a workflow, use one of the - following two calls: - - Supply ``workflow_id``, ``run_id``, and ``activity_id`` - - Supply the activity ``task_token`` alone - - To get a handle to an activity not started by a workflow, supply - ``activity_id`` and ``run_id`` - - Args: - workflow_id: Workflow ID for the activity, or None if not a workflow - activity. Cannot be set if task_token is set. - run_id: Run ID for the activity or workflow. Cannot be set if - task_token is set. - activity_id: ID for the activity. Cannot be set if task_token is - set. - task_token: Task token for the activity. Cannot be set with other - fields. - - Returns: - A handle that can be used for completion or heartbeating. - """ - if task_token is not None: - if workflow_id is not None or run_id is not None or activity_id is not None: - raise ValueError("Task token cannot be present with other IDs") - return AsyncActivityHandle(self, task_token) - elif workflow_id is not None: - if activity_id is None: - raise ValueError( - "Workflow ID, run ID, and activity ID must all be given together" - ) - return AsyncActivityHandle( - self, - AsyncActivityIDReference( - workflow_id=workflow_id, run_id=run_id, activity_id=activity_id - ), - ) - elif activity_id is not None: - return AsyncActivityHandle( - self, - AsyncActivityIDReference( - activity_id=activity_id, - run_id=run_id, - workflow_id=None, - ), - ) - raise ValueError( - "Require task token, or workflow_id & run_id & activity_id, or activity_id & run_id" - ) - - async def create_schedule( - self, - id: str, - schedule: Schedule, - *, - trigger_immediately: bool = False, - backfill: Sequence[ScheduleBackfill] = [], - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ScheduleHandle: - """Create a schedule and return its handle. - - Args: - id: Unique identifier of the schedule. - schedule: Schedule to create. - trigger_immediately: If true, trigger one action immediately when - creating the schedule. - backfill: Set of time periods to take actions on as if that time - passed right now. - memo: Memo for the schedule. Memo for a scheduled workflow is part - of the schedule action. - search_attributes: Search attributes for the schedule. Search - attributes for a scheduled workflow are part of the scheduled - action. The dictionary form of this is DEPRECATED, use - :py:class:`temporalio.common.TypedSearchAttributes`. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - A handle to the created schedule. - - Raises: - ScheduleAlreadyRunningError: If a schedule with this ID is already - running. - """ - temporalio.common._warn_on_deprecated_search_attributes(search_attributes) - return await self._impl.create_schedule( - CreateScheduleInput( - id=id, - schedule=schedule, - trigger_immediately=trigger_immediately, - backfill=backfill, - memo=memo, - search_attributes=search_attributes, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - def get_schedule_handle(self, id: str) -> ScheduleHandle: - """Get a schedule handle for the given ID.""" - return ScheduleHandle(self, id) - - async def list_schedules( - self, - query: str | None = None, - *, - page_size: int = 1000, - next_page_token: bytes | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ScheduleAsyncIterator: - """List schedules. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Note, this list is eventually consistent. Therefore if a schedule is - added or deleted, it may not be available in the list immediately. - - Args: - page_size: Maximum number of results for each page. - query: A Temporal visibility list filter. See Temporal documentation - concerning visibility list filters including behavior when left - unset. - next_page_token: A previously obtained next page token if doing - pagination. Usually not needed as the iterator automatically - starts from the beginning. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that can be used with ``async for``. - """ - return self._impl.list_schedules( - ListSchedulesInput( - page_size=page_size, - next_page_token=next_page_token, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - query=query, - ) - ) - - async def update_worker_build_id_compatibility( - self, - task_queue: str, - operation: BuildIdOp, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Used to add new Build IDs or otherwise update the relative compatibility of Build Ids as - defined on a specific task queue for the Worker Versioning feature. - - For more on this feature, see https://docs.temporal.io/workers#worker-versioning - - .. deprecated:: - Legacy API, see the docs above for new usage - - Args: - task_queue: The task queue to target. - operation: The operation to perform. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - """ - return await self._impl.update_worker_build_id_compatibility( - UpdateWorkerBuildIdCompatibilityInput( - task_queue, - operation, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def get_worker_build_id_compatibility( - self, - task_queue: str, - max_sets: int | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkerBuildIdVersionSets: - """Get the Build ID compatibility sets for a specific task queue. - - For more on this feature, see https://docs.temporal.io/workers#worker-versioning - - .. deprecated:: - Legacy API, see the docs above for new usage - - Args: - task_queue: The task queue to target. - max_sets: The maximum number of sets to return. If not specified, all sets will be - returned. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - """ - return await self._impl.get_worker_build_id_compatibility( - GetWorkerBuildIdCompatibilityInput( - task_queue, - max_sets, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def get_worker_task_reachability( - self, - build_ids: Sequence[str], - task_queues: Sequence[str] = [], - reachability_type: TaskReachabilityType | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkerTaskReachability: - """Determine if some Build IDs for certain Task Queues could have tasks dispatched to them. - - For more on this feature, see https://docs.temporal.io/workers#worker-versioning - - .. deprecated:: - Legacy API, see the docs above for new usage - - Args: - build_ids: The Build IDs to query the reachability of. At least one must be specified. - task_queues: Task Queues to restrict the query to. If not specified, all Task Queues - will be searched. When requesting a large number of task queues or all task queues - associated with the given Build IDs in a namespace, all Task Queues will be listed - in the response but some of them may not contain reachability information due to a - server enforced limit. When reaching the limit, task queues that reachability - information could not be retrieved for will be marked with a ``NotFetched`` entry in - {@link BuildIdReachability.taskQueueReachability}. The caller may issue another call - to get the reachability for those task queues. - reachability_type: The kind of reachability this request is concerned with. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - """ - return await self._impl.get_worker_task_reachability( - GetWorkerTaskReachabilityInput( - build_ids, - task_queues, - reachability_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - -class ClientConnectConfig(TypedDict, total=False): - """TypedDict of keyword arguments for :py:meth:`Client.connect`.""" - - target_host: str - namespace: str - api_key: str | None - data_converter: temporalio.converter.DataConverter - plugins: Sequence[Plugin] - interceptors: Sequence[Interceptor] - default_workflow_query_reject_condition: ( - temporalio.common.QueryRejectCondition | None - ) - tls: bool | TLSConfig | None - retry_config: RetryConfig | None - keep_alive_config: KeepAliveConfig | None - rpc_metadata: Mapping[str, str | bytes] - identity: str | None - lazy: bool - runtime: temporalio.runtime.Runtime | None - http_connect_proxy_config: HttpConnectProxyConfig | None - dns_load_balancing_config: DnsLoadBalancingConfig | None - header_codec_behavior: HeaderCodecBehavior - - -class ClientConfig(TypedDict, total=False): - """TypedDict of config originally passed to :py:meth:`Client`.""" - - service_client: Required[temporalio.service.ServiceClient] - namespace: Required[str] - data_converter: Required[temporalio.converter.DataConverter] - plugins: Required[Sequence[Plugin]] - interceptors: Required[Sequence[Interceptor]] - default_workflow_query_reject_condition: Required[ - temporalio.common.QueryRejectCondition | None - ] - header_codec_behavior: Required[HeaderCodecBehavior] - - -class WorkflowHistoryEventFilterType(IntEnum): - """Type of history events to get for a workflow. - - See :py:class:`temporalio.api.enums.v1.HistoryEventFilterType`. - """ - - ALL_EVENT = int( - temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT - ) - CLOSE_EVENT = int( - temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT - ) - - -class WorkflowHandle(Generic[SelfType, ReturnType]): - """Handle for interacting with a workflow. - - This is usually created via :py:meth:`Client.get_workflow_handle` or - returned from :py:meth:`Client.start_workflow`. - """ - - def __init__( - self, - client: Client, - id: str, - *, - run_id: str | None = None, - result_run_id: str | None = None, - first_execution_run_id: str | None = None, - result_type: type | None = None, - start_workflow_response: None - | ( - temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse - | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse - ) = None, - ) -> None: - """Create workflow handle.""" - self._client = client - self._id = id - self._run_id = run_id - self._result_run_id = result_run_id - self._first_execution_run_id = first_execution_run_id - self._result_type = result_type - self._start_workflow_response = start_workflow_response - self.__temporal_eagerly_started = False - - @functools.cached_property - def _data_converter(self) -> temporalio.converter.DataConverter: - return self._client.data_converter.with_context( - temporalio.converter.WorkflowSerializationContext( - namespace=self._client.namespace, workflow_id=self._id - ) - ) - - @property - def id(self) -> str: - """ID of the workflow.""" - return self._id - - @property - def run_id(self) -> str | None: - """If present, run ID used to ensure that requested operations apply - to this exact run. - - This is only created via :py:meth:`Client.get_workflow_handle`. - :py:meth:`Client.start_workflow` will not set this value. - - This cannot be mutated. If a different run ID is needed, - :py:meth:`Client.get_workflow_handle` must be used instead. - """ - return self._run_id - - @property - def result_run_id(self) -> str | None: - """Run ID used for :py:meth:`result` calls if present to ensure result - is for a workflow starting from this run. - - When this handle is created via :py:meth:`Client.get_workflow_handle`, - this is the same as run_id. When this handle is created via - :py:meth:`Client.start_workflow`, this value will be the resulting run - ID. - - This cannot be mutated. If a different run ID is needed, - :py:meth:`Client.get_workflow_handle` must be used instead. - """ - return self._result_run_id - - @property - def first_execution_run_id(self) -> str | None: - """Run ID used to ensure requested operations apply to a workflow ID - started with this run ID. - - This can be set when using :py:meth:`Client.get_workflow_handle`. When - :py:meth:`Client.start_workflow` is called without a start signal, this - is set to the resulting run. - - This cannot be mutated. If a different first execution run ID is needed, - :py:meth:`Client.get_workflow_handle` must be used instead. - """ - return self._first_execution_run_id - - async def result( - self, - *, - follow_runs: bool = True, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: - """Wait for result of the workflow. - - This will use :py:attr:`result_run_id` if present to base the result on. - To use another run ID, a new handle must be created via - :py:meth:`Client.get_workflow_handle`. - - Args: - follow_runs: If true (default), workflow runs will be continually - fetched, until the most recent one is found. If false, return - the result from the first run targeted by the request if that run - ends in a result, otherwise raise an exception. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. Note, - this is the timeout for each history RPC call not this overall - function. - - Returns: - Result of the workflow after being converted by the data converter. - - Raises: - WorkflowFailureError: Workflow failed, was cancelled, was - terminated, or timed out. Use the - :py:attr:`WorkflowFailureError.cause` to see the underlying - reason. - Exception: Other possible failures during result fetching. - """ - # We have to maintain our own run ID because it can change if we follow - # executions - hist_run_id = self._result_run_id - while True: - async for event in self._fetch_history_events_for_run( - hist_run_id, - wait_new_event=True, - event_filter_type=WorkflowHistoryEventFilterType.CLOSE_EVENT, - skip_archival=True, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ): - if event.HasField("workflow_execution_completed_event_attributes"): - complete_attr = event.workflow_execution_completed_event_attributes - # Follow execution - if follow_runs and complete_attr.new_execution_run_id: - hist_run_id = complete_attr.new_execution_run_id - break - # Ignoring anything after the first response like TypeScript - type_hints = [self._result_type] if self._result_type else None - results = await self._data_converter.decode_wrapper( - complete_attr.result, - type_hints, - ) - if not results: - return cast(ReturnType, None) - elif len(results) > 1: - warnings.warn(f"Expected single result, got {len(results)}") - return cast(ReturnType, results[0]) - elif event.HasField("workflow_execution_failed_event_attributes"): - fail_attr = event.workflow_execution_failed_event_attributes - # Follow execution - if follow_runs and fail_attr.new_execution_run_id: - hist_run_id = fail_attr.new_execution_run_id - break - raise WorkflowFailureError( - cause=await self._data_converter.decode_failure( - fail_attr.failure - ), - ) - elif event.HasField("workflow_execution_canceled_event_attributes"): - cancel_attr = event.workflow_execution_canceled_event_attributes - raise WorkflowFailureError( - cause=temporalio.exceptions.CancelledError( - "Workflow cancelled", - *( - await self._data_converter.decode_wrapper( - cancel_attr.details - ) - ), - ) - ) - elif event.HasField("workflow_execution_terminated_event_attributes"): - term_attr = event.workflow_execution_terminated_event_attributes - raise WorkflowFailureError( - cause=temporalio.exceptions.TerminatedError( - term_attr.reason or "Workflow terminated", - *( - await self._data_converter.decode_wrapper( - term_attr.details - ) - ), - ), - ) - elif event.HasField("workflow_execution_timed_out_event_attributes"): - time_attr = event.workflow_execution_timed_out_event_attributes - # Follow execution - if follow_runs and time_attr.new_execution_run_id: - hist_run_id = time_attr.new_execution_run_id - break - raise WorkflowFailureError( - cause=temporalio.exceptions.TimeoutError( - "Workflow timed out", - type=temporalio.exceptions.TimeoutType.START_TO_CLOSE, - last_heartbeat_details=[], - ), - ) - elif event.HasField( - "workflow_execution_continued_as_new_event_attributes" - ): - cont_attr = ( - event.workflow_execution_continued_as_new_event_attributes - ) - if not cont_attr.new_execution_run_id: - raise RuntimeError( - "Unexpectedly missing new run ID from continue as new" - ) - # Follow execution - if follow_runs: - hist_run_id = cont_attr.new_execution_run_id - break - raise WorkflowContinuedAsNewError(cont_attr.new_execution_run_id) - # This is reached on break which means that there's a different run - # ID if we're following. If there's not, it's an error because no - # event was given (should never happen). - if hist_run_id is None: - raise RuntimeError("No completion event found") - - async def cancel( - self, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Cancel the workflow. - - This will issue a cancellation for :py:attr:`run_id` if present. This - call will make sure to use the run chain starting from - :py:attr:`first_execution_run_id` if present. To create handles with - these values, use :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` with - a start signal will cancel the latest workflow with the same - workflow ID even if it is unrelated to the started workflow. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - RPCError: Workflow could not be cancelled. - """ - await self._client._impl.cancel_workflow( - CancelWorkflowInput( - id=self._id, - run_id=self._run_id, - first_execution_run_id=self._first_execution_run_id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def describe( - self, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowExecutionDescription: - """Get workflow details. - - This will get details for :py:attr:`run_id` if present. To use a - different run ID, create a new handle with via - :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - describe the latest workflow with the same workflow ID even if it is - unrelated to the started workflow. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - Workflow details. - - Raises: - RPCError: Workflow details could not be fetched. - """ - return await self._client._impl.describe_workflow( - DescribeWorkflowInput( - id=self._id, - run_id=self._run_id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def fetch_history( - self, - *, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowHistory: - """Get workflow history. - - This is a shortcut for :py:meth:`fetch_history_events` that just fetches - all events. - """ - return WorkflowHistory( - workflow_id=self.id, - events=[ - v - async for v in self.fetch_history_events( - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ], - ) - - def fetch_history_events( - self, - *, - page_size: int | None = None, - next_page_token: bytes | None = None, - wait_new_event: bool = False, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowHistoryEventAsyncIterator: - """Get workflow history events as an async iterator. - - This does not make a request until the first iteration is attempted. - Therefore any errors will not occur until then. - - Args: - page_size: Maximum amount to fetch per request if any maximum. - next_page_token: A specific page token to fetch. - wait_new_event: Whether the event fetching request will wait for new - events or just return right away. - event_filter_type: Which events to obtain. - skip_archival: Whether to skip archival. - rpc_metadata: Headers used on each RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. - - Returns: - An async iterator that doesn't begin fetching until iterated on. - """ - return self._fetch_history_events_for_run( - self._run_id, - page_size=page_size, - next_page_token=next_page_token, - wait_new_event=wait_new_event, - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - def _fetch_history_events_for_run( - self, - run_id: str | None, - *, - page_size: int | None = None, - next_page_token: bytes | None = None, - wait_new_event: bool = False, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowHistoryEventAsyncIterator: - return self._client._impl.fetch_workflow_history_events( - FetchWorkflowHistoryEventsInput( - id=self._id, - run_id=run_id, - page_size=page_size, - next_page_token=next_page_token, - wait_new_event=wait_new_event, - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - # Overload for no-param query - @overload - async def query( - self, - query: MethodSyncOrAsyncNoParam[SelfType, LocalReturnType], - *, - reject_condition: temporalio.common.QueryRejectCondition | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for single-param query - @overload - async def query( - self, - query: MethodSyncOrAsyncSingleParam[SelfType, ParamType, LocalReturnType], - arg: ParamType, - *, - reject_condition: temporalio.common.QueryRejectCondition | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for multi-param query - @overload - async def query( - self, - query: Callable[ - Concatenate[SelfType, MultiParamSpec], - Awaitable[LocalReturnType] | LocalReturnType, - ], - *, - args: Sequence[Any], - reject_condition: temporalio.common.QueryRejectCondition | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for string-name query - @overload - async def query( - self, - query: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: type | None = None, - reject_condition: temporalio.common.QueryRejectCondition | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: ... - - async def query( - self, - query: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - result_type: type | None = None, - reject_condition: temporalio.common.QueryRejectCondition | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: - """Query the workflow. - - This will query for :py:attr:`run_id` if present. To use a different - run ID, create a new handle with - :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - query the latest workflow with the same workflow ID even if it is - unrelated to the started workflow. - - Args: - query: Query function or name on the workflow. - arg: Single argument to the query. - args: Multiple arguments to the query. Cannot be set if arg is. - result_type: For string queries, this can set the specific result - type hint to deserialize into. - reject_condition: Condition for rejecting the query. If unset/None, - defaults to the client's default (which is defaulted to None). - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - Result of the query. - - Raises: - WorkflowQueryRejectedError: A query reject condition was satisfied. - RPCError: Workflow details could not be fetched. - """ - query_name: str - ret_type = result_type - if callable(query): - defn = temporalio.workflow._QueryDefinition.from_fn(query) - if not defn: - raise RuntimeError( - f"Query definition not found on {query.__qualname__}, " - "is it decorated with @workflow.query?" - ) - elif not defn.name: - raise RuntimeError("Cannot invoke dynamic query definition") - # TODO(cretz): Check count/type of args at runtime? - query_name = defn.name - ret_type = defn.ret_type - else: - query_name = str(query) - - return await self._client._impl.query_workflow( - QueryWorkflowInput( - id=self._id, - run_id=self._run_id, - query=query_name, - args=temporalio.common._arg_or_args(arg, args), - reject_condition=reject_condition - or self._client._config["default_workflow_query_reject_condition"], - headers={}, - ret_type=ret_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - # Overload for no-param signal - @overload - async def signal( - self, - signal: MethodSyncOrAsyncNoParam[SelfType, None], - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: ... - - # Overload for single-param signal - @overload - async def signal( - self, - signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], - arg: ParamType, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: ... - - # Overload for multi-param signal - @overload - async def signal( - self, - signal: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[None] | None], - *, - args: Sequence[Any], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: ... - - # Overload for string-name signal - @overload - async def signal( - self, - signal: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: ... - - async def signal( - self, - signal: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Send a signal to the workflow. - - This will signal for :py:attr:`run_id` if present. To use a different - run ID, create a new handle with via - :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` will - signal the latest workflow with the same workflow ID even if it is - unrelated to the started workflow. - - Args: - signal: Signal function or name on the workflow. - arg: Single argument to the signal. - args: Multiple arguments to the signal. Cannot be set if arg is. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - RPCError: Workflow could not be signalled. - """ - await self._client._impl.signal_workflow( - SignalWorkflowInput( - id=self._id, - run_id=self._run_id, - signal=temporalio.workflow._SignalDefinition.must_name_from_fn_or_str( - signal - ), - args=temporalio.common._arg_or_args(arg, args), - headers={}, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def terminate( - self, - *args: Any, - reason: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Terminate the workflow. - - This will issue a termination for :py:attr:`run_id` if present. This - call will make sure to use the run chain starting from - :py:attr:`first_execution_run_id` if present. To create handles with - these values, use :py:meth:`Client.get_workflow_handle`. - - .. warning:: - Handles created as a result of :py:meth:`Client.start_workflow` with - a start signal will terminate the latest workflow with the same - workflow ID even if it is unrelated to the started workflow. - - Args: - args: Details to store on the termination. - reason: Reason for the termination. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - RPCError: Workflow could not be terminated. - """ - await self._client._impl.terminate_workflow( - TerminateWorkflowInput( - id=self._id, - run_id=self._run_id, - args=args, - reason=reason, - first_execution_run_id=self._first_execution_run_id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - # Overload for no-param update - @overload - async def execute_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for single-param update - @overload - async def execute_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for multi-param update - @overload - async def execute_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # type: ignore - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: ... - - # Overload for string-name update - @overload - async def execute_update( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: ... - - async def execute_update( - self, - update: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> Any: - """Send an update request to the workflow and wait for it to complete. - - This will target the workflow with :py:attr:`run_id` if present. To use a - different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. - - Args: - update: Update function or name on the workflow. - arg: Single argument to the update. - args: Multiple arguments to the update. Cannot be set if arg is. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed - out or cancelled. - RPCError: There was some issue sending the update to the workflow. - """ - handle = await self._start_update( - update, - arg, - args=args, - wait_for_stage=WorkflowUpdateStage.COMPLETED, - id=id, - result_type=result_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - return await handle.result() - - # Overload for no-param start update - @overload - async def start_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], - *, - wait_for_stage: WorkflowUpdateStage, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for single-param start update - @overload - async def start_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - [SelfType, ParamType], LocalReturnType - ], - arg: ParamType, - *, - wait_for_stage: WorkflowUpdateStage, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for multi-param start update - @overload - async def start_update( - self, - update: temporalio.workflow.UpdateMethodMultiParam[ - MultiParamSpec, LocalReturnType - ], - *, - args: MultiParamSpec.args, # type: ignore - wait_for_stage: WorkflowUpdateStage, - id: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: ... - - # Overload for string-name start update - @overload - async def start_update( - self, - update: str, - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[Any]: ... - - async def start_update( - self, - update: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[Any]: - """Send an update request to the workflow and return a handle to it. - - This will target the workflow with :py:attr:`run_id` if present. To use a - different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. - - Args: - update: Update function or name on the workflow. arg: Single argument to the - update. - wait_for_stage: Required stage to wait until returning: either ACCEPTED or - COMPLETED. ADMITTED is not currently supported. See - https://docs.temporal.io/workflows#update for more details. - args: Multiple arguments to the update. Cannot be set if arg is. - id: ID of the update. If not set, the default is a new UUID. - result_type: For string updates, this can set the specific result - type hint to deserialize into. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Raises: - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed out or - cancelled. - RPCError: There was some issue sending the update to the workflow. - """ - return await self._start_update( - update, - arg, - wait_for_stage=wait_for_stage, - args=args, - id=id, - result_type=result_type, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - async def _start_update( - self, - update: str | Callable, - arg: Any = temporalio.common._arg_unset, - *, - wait_for_stage: WorkflowUpdateStage, - args: Sequence[Any] = [], - id: str | None = None, - result_type: type | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> WorkflowUpdateHandle[Any]: - if wait_for_stage == WorkflowUpdateStage.ADMITTED: - raise ValueError("ADMITTED wait stage not supported") - - update_name, result_type_from_type_hint = ( - temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) - ) - - return await self._client._impl.start_workflow_update( - StartWorkflowUpdateInput( - id=self._id, - run_id=self._run_id, - first_execution_run_id=self.first_execution_run_id, - update_id=id, - update=update_name, - args=temporalio.common._arg_or_args(arg, args), - headers={}, - ret_type=result_type or result_type_from_type_hint, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - wait_for_stage=wait_for_stage, - ) - ) - - def get_update_handle( - self, - id: str, - *, - workflow_run_id: str | None = None, - result_type: type | None = None, - ) -> WorkflowUpdateHandle[Any]: - """Get a handle for an update. The handle can be used to wait on the - update result. - - Users may prefer the more typesafe :py:meth:`get_update_handle_for` - which accepts an update definition. - - Args: - id: Update ID to get a handle to. - workflow_run_id: Run ID to tie the handle to. If this is not set, - the :py:attr:`run_id` will be used. - result_type: The result type to deserialize into if known. - - Returns: - The update handle. - """ - return WorkflowUpdateHandle( - self._client, - id, - self._id, - workflow_run_id=workflow_run_id or self._run_id, - result_type=result_type, - ) - - def get_update_handle_for( - self, - update: temporalio.workflow.UpdateMethodMultiParam[Any, LocalReturnType], - id: str, - *, - workflow_run_id: str | None = None, - ) -> WorkflowUpdateHandle[LocalReturnType]: - """Get a typed handle for an update. The handle can be used to wait on - the update result. - - This is the same as :py:meth:`get_update_handle` but typed. - - Args: - update: The update method to use for typing the handle. - id: Update ID to get a handle to. - workflow_run_id: Run ID to tie the handle to. If this is not set, - the :py:attr:`run_id` will be used. - - Returns: - The update handle. - """ - return self.get_update_handle( - id, workflow_run_id=workflow_run_id, result_type=update._defn.ret_type - ) - - -class WithStartWorkflowOperation(Generic[SelfType, ReturnType]): - """Defines a start-workflow operation used by update-with-start requests. - - Update-With-Start allows you to send an update to a workflow, while starting the - workflow if necessary. - """ - - # Overload for no-param workflow, with_start - @overload - def __init__( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> None: ... - - # Overload for single-param workflow, with_start - @overload - def __init__( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> None: ... - - # Overload for multi-param workflow, with_start - @overload - def __init__( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> None: ... - - # Overload for string-name workflow, with_start - @overload - def __init__( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - result_type: type | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - ) -> None: ... - - def __init__( - self, - workflow: str | Callable[..., Awaitable[Any]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, - result_type: type | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, - retry_policy: temporalio.common.RetryPolicy | None = None, - cron_schedule: str = "", - memo: Mapping[str, Any] | None = None, - search_attributes: None - | ( - temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes - ) = None, - static_summary: str | None = None, - static_details: str | None = None, - start_delay: timedelta | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - versioning_override: temporalio.common.VersioningOverride | None = None, - stack_level: int = 2, - ) -> None: - """Create a WithStartWorkflowOperation. - - See :py:meth:`temporalio.client.Client.start_workflow` for documentation of the - arguments. - """ - temporalio.common._warn_on_deprecated_search_attributes( - search_attributes, stack_level=stack_level - ) - name, result_type_from_run_fn = ( - temporalio.workflow._Definition.get_name_and_result_type(workflow) - ) - if id_conflict_policy == temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED: - raise ValueError("WorkflowIDConflictPolicy is required") - - self._start_workflow_input = UpdateWithStartStartWorkflowInput( - workflow=name, - args=temporalio.common._arg_or_args(arg, args), - id=id, - task_queue=task_queue, - execution_timeout=execution_timeout, - run_timeout=run_timeout, - task_timeout=task_timeout, - id_reuse_policy=id_reuse_policy, - id_conflict_policy=id_conflict_policy, - retry_policy=retry_policy, - cron_schedule=cron_schedule, - memo=memo, - search_attributes=search_attributes, - static_summary=static_summary, - static_details=static_details, - start_delay=start_delay, - headers={}, - ret_type=result_type or result_type_from_run_fn, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - priority=priority, - versioning_override=versioning_override, - ) - self._workflow_handle: Future[WorkflowHandle[SelfType, ReturnType]] = Future() - self._used = False - - async def workflow_handle(self) -> WorkflowHandle[SelfType, ReturnType]: - """Wait until workflow is running and return a WorkflowHandle.""" - return await self._workflow_handle - - -class ActivityExecutionAsyncIterator: - """Asynchronous iterator for activity execution values. - - You should typically use ``async for`` on this iterator and not call any of its methods. - - .. warning:: - This API is experimental. - """ - - def __init__( - self, - client: Client, - input: ListActivitiesInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`Client.list_activities`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: Sequence[ActivityExecution] | None = None - self._current_page_index = 0 - self._limit = input.limit - self._yielded = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page(self) -> Sequence[ActivityExecution] | None: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> bytes | None: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: int | None = None) -> None: - """Fetch the next page of results. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - page_size = page_size or self._input.page_size - if self._limit is not None and self._limit - self._yielded < page_size: - page_size = self._limit - self._yielded - - resp = await self._client.workflow_service.list_activity_executions( - temporalio.api.workflowservice.v1.ListActivityExecutionsRequest( - namespace=self._client.namespace, - page_size=page_size, - next_page_token=self._next_page_token or b"", - query=self._input.query or "", - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - - self._current_page = [ - ActivityExecution._from_raw_info(v, self._client.namespace) - for v in resp.executions - ] - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> ActivityExecutionAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> ActivityExecution: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - if self._limit is not None and self._yielded >= self._limit: - raise StopAsyncIteration - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Get current, increment page index, and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - self._yielded += 1 - return ret - - -@dataclass(frozen=True) -class ActivityExecution: - """Info for an activity execution not started by a workflow, from list response. - - .. warning:: - This API is experimental. - """ - - activity_id: str - """Activity ID.""" - - activity_run_id: str | None - """Run ID of the activity.""" - - activity_type: str - """Type name of the activity.""" - - close_time: datetime | None - """Time the activity reached a terminal status, if closed.""" - - execution_duration: timedelta | None - """Duration from scheduled to close time, only populated if closed.""" - - namespace: str - """Namespace of the activity (copied from calling client).""" - - raw_info: ( - temporalio.api.activity.v1.ActivityExecutionListInfo - | temporalio.api.activity.v1.ActivityExecutionInfo - ) - """Underlying protobuf info.""" - - scheduled_time: datetime - """Time the activity was originally scheduled.""" - - state_transition_count: int | None - """Number of state transitions, if available.""" - - status: ActivityExecutionStatus - """Current status of the activity.""" - - task_queue: str - """Task queue the activity was scheduled on.""" - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Current set of search attributes if any.""" - - @classmethod - def _from_raw_info( - cls, info: temporalio.api.activity.v1.ActivityExecutionListInfo, namespace: str - ) -> Self: - """Create from raw proto activity list info.""" - return cls( - activity_id=info.activity_id, - activity_run_id=info.run_id or None, - activity_type=( - info.activity_type.name if info.HasField("activity_type") else "" - ), - close_time=( - info.close_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("close_time") - else None - ), - execution_duration=( - info.execution_duration.ToTimedelta() - if info.HasField("execution_duration") - else None - ), - namespace=namespace, - raw_info=info, - scheduled_time=( - info.schedule_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("schedule_time") - else datetime.min - ), - state_transition_count=( - info.state_transition_count if info.state_transition_count else None - ), - status=( - ActivityExecutionStatus(info.status) - if info.status - else ActivityExecutionStatus.UNSPECIFIED - ), - task_queue=info.task_queue, - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - info.search_attributes - ), - ) - - -@dataclass(frozen=True) -class ActivityExecutionDescription(ActivityExecution): - """Detailed information about an activity execution not started by a workflow. - - .. warning:: - This API is experimental. - """ - - attempt: int - """Current attempt number.""" - - canceled_reason: str | None - """Reason for cancellation, if cancel was requested.""" - - current_retry_interval: timedelta | None - """Time until the next retry, if applicable.""" - - eager_execution_requested: bool - """Whether eager execution was requested for this activity.""" - - expiration_time: datetime - """Scheduled time plus schedule_to_close_timeout.""" - - last_attempt_complete_time: datetime | None - """Time when the last attempt completed.""" - - last_failure: Exception | None - """Failure from the last failed attempt, if any.""" - - last_heartbeat_time: datetime | None - """Time of the last heartbeat.""" - - last_started_time: datetime | None - """Time the last attempt was started.""" - - last_worker_identity: str - """Identity of the last worker that processed the activity.""" - - next_attempt_schedule_time: datetime | None - """Time when the next attempt will be scheduled.""" - - paused: bool - """Whether the activity is paused.""" - - raw_heartbeat_details: Sequence[temporalio.api.common.v1.Payload] - """Details from the last heartbeat.""" - - retry_policy: temporalio.common.RetryPolicy | None - """Retry policy for the activity.""" - - run_state: PendingActivityState | None - """More detailed breakdown if status is RUNNING.""" - - long_poll_token: bytes | None - """Token for follow-on long-poll requests. None if the activity is complete.""" - - @classmethod - async def _from_execution_info( - cls, - info: temporalio.api.activity.v1.ActivityExecutionInfo, - long_poll_token: bytes | None, - namespace: str, - data_converter: temporalio.converter.DataConverter, - ) -> Self: - """Create from raw proto activity execution info.""" - # Decode heartbeat details if present - decoded_heartbeat_details: Sequence[temporalio.api.common.v1.Payload] = ( - info.heartbeat_details.payloads - ) - if decoded_heartbeat_details and data_converter.payload_codec: - decoded_heartbeat_details = await data_converter.payload_codec.decode( - decoded_heartbeat_details - ) - - return cls( - activity_id=info.activity_id, - activity_run_id=info.run_id or None, - activity_type=( - info.activity_type.name if info.HasField("activity_type") else "" - ), - attempt=info.attempt, - canceled_reason=info.canceled_reason or None, - close_time=( - info.close_time.ToDatetime(tzinfo=timezone.utc) - if info.HasField("close_time") - else None - ), - current_retry_interval=( - info.current_retry_interval.ToTimedelta() - if info.HasField("current_retry_interval") - else None - ), - eager_execution_requested=getattr(info, "eager_execution_requested", False), - execution_duration=( - info.execution_duration.ToTimedelta() - if info.HasField("execution_duration") - else None - ), - expiration_time=( - info.expiration_time.ToDatetime(tzinfo=timezone.utc) - if info.HasField("expiration_time") - else datetime.min - ), - last_attempt_complete_time=( - info.last_attempt_complete_time.ToDatetime(tzinfo=timezone.utc) - if info.HasField("last_attempt_complete_time") - else None - ), - last_failure=( - cast( - Exception | None, - await data_converter.decode_failure(info.last_failure), - ) - if info.HasField("last_failure") - else None - ), - last_heartbeat_time=( - info.last_heartbeat_time.ToDatetime(tzinfo=timezone.utc) - if info.HasField("last_heartbeat_time") - else None - ), - last_started_time=( - info.last_started_time.ToDatetime(tzinfo=timezone.utc) - if info.HasField("last_started_time") - else None - ), - last_worker_identity=info.last_worker_identity, - long_poll_token=long_poll_token or None, - namespace=namespace, - next_attempt_schedule_time=( - info.next_attempt_schedule_time.ToDatetime(tzinfo=timezone.utc) - if info.HasField("next_attempt_schedule_time") - else None - ), - paused=getattr(info, "paused", False), - raw_heartbeat_details=decoded_heartbeat_details, - raw_info=info, - retry_policy=temporalio.common.RetryPolicy.from_proto(info.retry_policy) - if info.HasField("retry_policy") - else None, - run_state=( - PendingActivityState(info.run_state) if info.run_state else None - ), - scheduled_time=(info.schedule_time.ToDatetime(tzinfo=timezone.utc)), - state_transition_count=( - info.state_transition_count if info.state_transition_count else None - ), - status=( - ActivityExecutionStatus(info.status) - if info.status - else ActivityExecutionStatus.UNSPECIFIED - ), - task_queue=info.task_queue, - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - info.search_attributes - ), - ) - - -class ActivityExecutionStatus(IntEnum): - """Status of an activity execution. - - .. warning:: - This API is experimental. - - See :py:class:`temporalio.api.enums.v1.ActivityExecutionStatus`. - """ - - UNSPECIFIED = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_UNSPECIFIED - ) - RUNNING = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_RUNNING - ) - COMPLETED = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_COMPLETED - ) - FAILED = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_FAILED - ) - CANCELED = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_CANCELED - ) - TERMINATED = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_TERMINATED - ) - TIMED_OUT = int( - temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_TIMED_OUT - ) - - -class PendingActivityState(IntEnum): - """Detailed state of an activity execution that is in ACTIVITY_EXECUTION_STATUS_RUNNING. - - .. warning:: - This API is experimental. - - See :py:class:`temporalio.api.enums.v1.PendingActivityState`. - """ - - UNSPECIFIED = int( - temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_UNSPECIFIED - ) - SCHEDULED = int( - temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_SCHEDULED - ) - STARTED = int( - temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_STARTED - ) - CANCEL_REQUESTED = int( - temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_CANCEL_REQUESTED - ) - PAUSED = int( - temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_PAUSED - ) - PAUSE_REQUESTED = int( - temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED - ) - - -@dataclass(frozen=True) -class ActivityExecutionCount: - """Representation of a count from a count activities call. - - .. warning:: - This API is experimental. - """ - - count: int - """Total count matching the filter, if any.""" - - groups: Sequence[ActivityExecutionCountAggregationGroup] - """Aggregation groups if requested.""" - - @staticmethod - def _from_raw( - resp: temporalio.api.workflowservice.v1.CountActivityExecutionsResponse, - ) -> ActivityExecutionCount: - """Create from raw proto response.""" - return ActivityExecutionCount( - count=resp.count, - groups=[ - ActivityExecutionCountAggregationGroup._from_raw(g) for g in resp.groups - ], - ) - - -@dataclass(frozen=True) -class ActivityExecutionCountAggregationGroup: - """A single aggregation group from a count activities call. - - .. warning:: - This API is experimental. - """ - - count: int - """Count for this group.""" - - group_values: Sequence[temporalio.common.SearchAttributeValue] - """Values that define this group.""" - - @staticmethod - def _from_raw( - raw: temporalio.api.workflowservice.v1.CountActivityExecutionsResponse.AggregationGroup, - ) -> ActivityExecutionCountAggregationGroup: - return ActivityExecutionCountAggregationGroup( - count=raw.count, - group_values=[ - temporalio.converter._search_attributes._decode_search_attribute_value( - v - ) - for v in raw.group_values - ], - ) - - -@dataclass(frozen=True) -class AsyncActivityIDReference: - """Reference to an async activity by its qualified ID.""" - - workflow_id: str | None - run_id: str | None - activity_id: str - - -class AsyncActivityHandle(WithSerializationContext): - """Handle representing an external activity for completion and heartbeat.""" - - def __init__( - self, - client: Client, - id_or_token: AsyncActivityIDReference | bytes, - data_converter_override: DataConverter | None = None, - ) -> None: - """Create an async activity handle.""" - self._client = client - self._id_or_token = id_or_token - self._data_converter_override = data_converter_override - - async def heartbeat( - self, - *details: Any, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Record a heartbeat for the activity. - - Args: - details: Details of the heartbeat. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.heartbeat_async_activity( - HeartbeatAsyncActivityInput( - id_or_token=self._id_or_token, - details=details, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - data_converter_override=self._data_converter_override, - ), - ) - - async def complete( - self, - result: Any | None = temporalio.common._arg_unset, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Complete the activity. - - Args: - result: Result of the activity if any. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.complete_async_activity( - CompleteAsyncActivityInput( - id_or_token=self._id_or_token, - result=result, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - data_converter_override=self._data_converter_override, - ), - ) - - async def fail( - self, - error: Exception, - *, - last_heartbeat_details: Sequence[Any] = [], - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Fail the activity. - - Args: - error: Error for the activity. - last_heartbeat_details: Last heartbeat details for the activity. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.fail_async_activity( - FailAsyncActivityInput( - id_or_token=self._id_or_token, - error=error, - last_heartbeat_details=last_heartbeat_details, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - data_converter_override=self._data_converter_override, - ), - ) - - async def report_cancellation( - self, - *details: Any, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Report the activity as cancelled. - - Args: - details: Cancellation details. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.report_cancellation_async_activity( - ReportCancellationAsyncActivityInput( - id_or_token=self._id_or_token, - details=details, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - data_converter_override=self._data_converter_override, - ), - ) - - def with_context(self, context: SerializationContext) -> Self: - """Create a new AsyncActivityHandle with a different serialization context. - - Payloads received by the activity will be decoded and deserialized using a data converter - with :py:class:`ActivitySerializationContext` set as context. If you are using a custom data - converter that makes use of this context then you can use this method to supply matching - context data to the data converter used to serialize and encode the outbound payloads. - """ - data_converter = self._client.data_converter.with_context(context) - if data_converter is self._client.data_converter: - return self - cls = type(self) - if cls.__init__ is not AsyncActivityHandle.__init__: - raise TypeError( - "If you have subclassed AsyncActivityHandle and overridden the __init__ method " - "then you must override with_context to return an instance of your class." - ) - return cls( - self._client, - self._id_or_token, - data_converter, - ) - - -class ActivityHandle(Generic[ReturnType]): - """Handle representing an activity execution not started by a workflow. - - .. warning:: - This API is experimental. - """ - - def __init__( - self, - client: Client, - id: str, - *, - run_id: str | None = None, - result_type: type | None = None, - ) -> None: - """Create activity handle.""" - self._client = client - self._id = id - self._run_id = run_id - self._result_type = result_type - self._known_outcome: ( - temporalio.api.activity.v1.ActivityExecutionOutcome | None - ) = None - - @functools.cached_property - def _data_converter(self) -> temporalio.converter.DataConverter: - return self._client.data_converter.with_context( - ActivitySerializationContext( - namespace=self._client.namespace, - activity_id=self._id, - activity_type=None, - activity_task_queue=None, - is_local=False, - workflow_id=None, - workflow_type=None, - ) - ) - - @property - def id(self) -> str: - """ID of the activity.""" - return self._id - - @property - def run_id(self) -> str | None: - """Run ID of the activity.""" - return self._run_id - - async def result( - self, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ReturnType: - """Wait for result of the activity. - - .. warning:: - This API is experimental. - - The result may already be known if this method has been called before, - in which case no network call is made. Otherwise the result will be - polled for until it is available. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. Note: - this is the timeout for each RPC call while polling, not a - timeout for the function as a whole. If an individual RPC - times out, it will be retried until the result is available. - - Returns: - The result of the activity. - - Raises: - ActivityFailureError: If the activity completed with a failure. - RPCError: Activity result could not be fetched for some reason. - """ - await self._poll_until_outcome( - rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout - ) - - # Convert outcome to failure or value - assert self._known_outcome - if self._known_outcome.HasField("failure"): - raise ActivityFailureError( - cause=await self._data_converter.decode_failure( - self._known_outcome.failure - ), - ) - if not self._known_outcome.result.payloads: - return None # type: ignore - type_hints = [self._result_type] if self._result_type else None - results = await self._data_converter.decode( - self._known_outcome.result.payloads, type_hints - ) - if not results: - return None # type: ignore - elif len(results) > 1: - warnings.warn(f"Expected single activity result, got {len(results)}") - return results[0] - - async def _poll_until_outcome( - self, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Poll for activity result until it's available.""" - if self._known_outcome: - return - - req = temporalio.api.workflowservice.v1.PollActivityExecutionRequest( - namespace=self._client.namespace, - activity_id=self._id, - run_id=self._run_id or "", - ) - - # Continue polling as long as we have no outcome - while True: - try: - res = await self._client.workflow_service.poll_activity_execution( - req, - retry=True, - metadata=rpc_metadata, - timeout=rpc_timeout, - ) - if res.HasField("outcome"): - self._known_outcome = res.outcome - return - except RPCError as err: - if err.status == RPCStatusCode.DEADLINE_EXCEEDED: - # Deadline exceeded is expected with long polling; retry - continue - elif err.status == RPCStatusCode.CANCELLED: - raise asyncio.CancelledError() from err - else: - raise - except asyncio.CancelledError: - raise - - async def cancel( - self, - *, - reason: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Request cancellation of the activity. - - .. warning:: - This API is experimental. - - Requesting cancellation of an activity does not automatically transition the activity to - canceled status. If the activity is heartbeating, a :py:class:`exceptions.CancelledError` - exception will be raised when receiving the heartbeat response; if the activity allows this - exception to bubble out, the activity will transition to canceled status. If the activity it - is not heartbeating, this method will have no effect on activity status. - - Args: - reason: Reason for the cancellation. Recorded and available via describe. - rpc_metadata: Headers used on the RPC call. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.cancel_activity( - CancelActivityInput( - activity_id=self._id, - activity_run_id=self._run_id, - reason=reason, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def terminate( - self, - *, - reason: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Terminate the activity execution immediately. - - .. warning:: - This API is experimental. - - Termination does not reach the worker and the activity code cannot react to it. - A terminated activity may have a running attempt and will be requested to be - canceled by the server when it heartbeats. - - Args: - reason: Reason for the termination. - rpc_metadata: Headers used on the RPC call. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.terminate_activity( - TerminateActivityInput( - activity_id=self._id, - activity_run_id=self._run_id, - reason=reason, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - async def describe( - self, - *, - long_poll_token: bytes | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ActivityExecutionDescription: - """Describe the activity execution. - - .. warning:: - This API is experimental. - - Args: - long_poll_token: Token from a previous describe response. If provided, - the request will long-poll until the activity state changes. - rpc_metadata: Headers used on the RPC call. - rpc_timeout: Optional RPC deadline to set for the RPC call. - - Returns: - Activity execution description. - """ - return await self._client._impl.describe_activity( - DescribeActivityInput( - activity_id=self._id, - activity_run_id=self._run_id, - long_poll_token=long_poll_token, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - ) - - -@dataclass -class WorkflowExecution: - """Info for a single workflow execution run.""" - - close_time: datetime | None - """When the workflow was closed if closed.""" - - execution_time: datetime | None - """When this workflow run started or should start.""" - - history_length: int - """Number of events in the history.""" - - id: str - """ID for the workflow.""" - - namespace: str - """Namespace for the workflow.""" - - parent_id: str | None - """ID for the parent workflow if this was started as a child.""" - - parent_run_id: str | None - """Run ID for the parent workflow if this was started as a child.""" - - root_id: str | None - """ID for the root workflow.""" - - root_run_id: str | None - """Run ID for the root workflow.""" - - raw_info: temporalio.api.workflow.v1.WorkflowExecutionInfo - """Underlying protobuf info.""" - - run_id: str - """Run ID for this workflow run.""" - - search_attributes: temporalio.common.SearchAttributes - """Current set of search attributes if any. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - start_time: datetime - """When the workflow was created.""" - - status: WorkflowExecutionStatus | None - """Status for the workflow.""" - - task_queue: str - """Task queue for the workflow.""" - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Current set of search attributes if any.""" - - workflow_type: str - """Type name for the workflow.""" - - _context_free_data_converter: temporalio.converter.DataConverter - - @property - def data_converter(self) -> temporalio.converter.DataConverter: - """Data converter for the workflow.""" - return self._context_free_data_converter.with_context( - WorkflowSerializationContext( - namespace=self.namespace, - workflow_id=self.id, - ) - ) - - @classmethod - def _from_raw_info( - cls, - info: temporalio.api.workflow.v1.WorkflowExecutionInfo, - namespace: str, - converter: temporalio.converter.DataConverter, - **additional_fields: Any, - ) -> Self: - return cls( - close_time=( - info.close_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("close_time") - else None - ), - execution_time=( - info.execution_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("execution_time") - else None - ), - history_length=info.history_length, - id=info.execution.workflow_id, - namespace=namespace, - parent_id=( - info.parent_execution.workflow_id - if info.HasField("parent_execution") - else None - ), - parent_run_id=( - info.parent_execution.run_id - if info.HasField("parent_execution") - else None - ), - root_id=( - info.root_execution.workflow_id - if info.HasField("root_execution") - else None - ), - root_run_id=( - info.root_execution.run_id if info.HasField("root_execution") else None - ), - raw_info=info, - run_id=info.execution.run_id, - search_attributes=temporalio.converter.decode_search_attributes( - info.search_attributes - ), - start_time=info.start_time.ToDatetime().replace(tzinfo=timezone.utc), - status=WorkflowExecutionStatus(info.status) if info.status else None, - task_queue=info.task_queue, - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - info.search_attributes - ), - workflow_type=info.type.name, - _context_free_data_converter=converter, - **additional_fields, - ) - - async def memo(self) -> Mapping[str, Any]: - """Workflow's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come - back. For example, if the memo was originally created with a dataclass, - the value will be a dict. To convert using proper type hints, use - :py:meth:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return await self.data_converter._decode_memo(self.raw_info.memo) - - @overload - async def memo_value( - self, key: str, default: Any = temporalio.common._arg_unset - ) -> Any: ... - - @overload - async def memo_value( - self, key: str, *, type_hint: type[ParamType] - ) -> ParamType: ... - - @overload - async def memo_value( - self, key: str, default: AnyType, *, type_hint: type[ParamType] - ) -> AnyType | ParamType: ... - - async def memo_value( - self, - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: type | None = None, - ) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - return await self.data_converter._decode_memo_field( - self.raw_info.memo, key, default, type_hint - ) - - -@dataclass -class WorkflowExecutionDescription(WorkflowExecution): - """Description for a single workflow execution run.""" - - raw_description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse - """Underlying protobuf description.""" - - _static_summary: str | None = None - _static_details: str | None = None - _metadata_decoded: bool = False - - async def static_summary(self) -> str | None: - """Gets the single-line fixed summary for this workflow execution that may appear in - UI/CLI. This can be in single-line Temporal markdown format. - """ - if not self._metadata_decoded: - await self._decode_metadata() - return self._static_summary - - async def static_details(self) -> str | None: - """Gets the general fixed details for this workflow execution that may appear in UI/CLI. - This can be in Temporal markdown format and can span multiple lines. - """ - if not self._metadata_decoded: - await self._decode_metadata() - return self._static_details - - async def _decode_metadata(self) -> None: - """Internal method to decode metadata lazily.""" - self._static_summary, self._static_details = await _decode_user_metadata( - self.data_converter, self.raw_description.execution_config.user_metadata - ) - self._metadata_decoded = True - - @staticmethod - async def _from_raw_description( - description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse, - namespace: str, - converter: temporalio.converter.DataConverter, - ) -> WorkflowExecutionDescription: - return WorkflowExecutionDescription._from_raw_info( - description.workflow_execution_info, - namespace=namespace, - converter=converter, - raw_description=description, - ) - - -class WorkflowExecutionStatus(IntEnum): - """Status of a workflow execution. - - See :py:class:`temporalio.api.enums.v1.WorkflowExecutionStatus`. - """ - - RUNNING = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING - ) - COMPLETED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED - ) - FAILED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED - ) - CANCELED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED - ) - TERMINATED = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED - ) - CONTINUED_AS_NEW = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW - ) - TIMED_OUT = int( - temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT - ) - - -@dataclass -class WorkflowExecutionCount: - """Representation of a count from a count workflows call.""" - - count: int - """Approximate number of workflows matching the original query. - - If the query had a group-by clause, this is simply the sum of all the counts - in py:attr:`groups`. - """ - - groups: Sequence[WorkflowExecutionCountAggregationGroup] - """Groups if the query had a group-by clause, or empty if not.""" - - @staticmethod - def _from_raw( - raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse, - ) -> WorkflowExecutionCount: - return WorkflowExecutionCount( - count=raw.count, - groups=[ - WorkflowExecutionCountAggregationGroup._from_raw(g) for g in raw.groups - ], - ) - - -@dataclass -class WorkflowExecutionCountAggregationGroup: - """Aggregation group if the workflow count query had a group-by clause.""" - - count: int - """Approximate number of workflows matching the original query for this - group. - """ - - group_values: Sequence[temporalio.common.SearchAttributeValue] - """Search attribute values for this group.""" - - @staticmethod - def _from_raw( - raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup, - ) -> WorkflowExecutionCountAggregationGroup: - return WorkflowExecutionCountAggregationGroup( - count=raw.count, - group_values=[ - temporalio.converter._search_attributes._decode_search_attribute_value( - v - ) - for v in raw.group_values - ], - ) - - -class WorkflowExecutionAsyncIterator: - """Asynchronous iterator for :py:class:`WorkflowExecution` values. - - Most users should use ``async for`` on this iterator and not call any of the - methods within. To consume the workflows as histories, call - :py:meth:`map_histories`. - """ - - def __init__( - self, - client: Client, - input: ListWorkflowsInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`Client.list_workflows`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: Sequence[WorkflowExecution] | None = None - self._current_page_index = 0 - self._limit = input.limit - self._yielded = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page(self) -> Sequence[WorkflowExecution] | None: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> bytes | None: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: int | None = None) -> None: - """Fetch the next page if any. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - page_size = page_size or self._input.page_size - if self._limit is not None and self._limit - self._yielded < page_size: - page_size = self._limit - self._yielded - - resp = await self._client.workflow_service.list_workflow_executions( - temporalio.api.workflowservice.v1.ListWorkflowExecutionsRequest( - namespace=self._client.namespace, - page_size=page_size, - next_page_token=self._next_page_token or b"", - query=self._input.query or "", - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - - self._current_page = [ - WorkflowExecution._from_raw_info( - v, self._client.namespace, self._client.data_converter - ) - for v in resp.executions - ] - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> WorkflowExecutionAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> WorkflowExecution: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - if self._limit is not None and self._yielded >= self._limit: - raise StopAsyncIteration - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Get current, increment page index, and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - self._yielded += 1 - return ret - - async def map_histories( - self, - *, - event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, - skip_archival: bool = False, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> AsyncIterator[WorkflowHistory]: - """Create an async iterator consuming all workflows and calling - :py:meth:`WorkflowHandle.fetch_history` on each one. - - This is just a shortcut for ``fetch_history``, see that method for - parameter details. - """ - async for v in self: - yield await self._client.get_workflow_handle( - v.id, run_id=v.run_id - ).fetch_history( - event_filter_type=event_filter_type, - skip_archival=skip_archival, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ) - - -@dataclass(frozen=True) -class WorkflowHistory: - """A workflow's ID and immutable history.""" - - workflow_id: str - """ID of the workflow.""" - - events: Sequence[temporalio.api.history.v1.HistoryEvent] - """History events for the workflow.""" - - @property - def run_id(self) -> str: - """Run ID extracted from the first event.""" - if not self.events: - raise RuntimeError("No events") - if not self.events[0].HasField("workflow_execution_started_event_attributes"): - raise RuntimeError("First event is not workflow start") - return self.events[ - 0 - ].workflow_execution_started_event_attributes.original_execution_run_id - - @staticmethod - def from_json(workflow_id: str, history: str | dict[str, Any]) -> WorkflowHistory: - """Construct a WorkflowHistory from an ID and a json dump of history. - - This is built to work both with Temporal UI/CLI JSON as well as - :py:meth:`to_json` even though they are slightly different. - - Args: - workflow_id: The workflow's ID - history: A string or parsed-to-dict representation of workflow - history - - Returns: - Workflow history - """ - parsed = _history_from_json(history) - return WorkflowHistory(workflow_id, parsed.events) - - def to_json(self) -> str: - """Convert this history to JSON. - - Note, this does not include the workflow ID. - """ - return google.protobuf.json_format.MessageToJson( - temporalio.api.history.v1.History(events=self.events) - ) - - def to_json_dict(self) -> dict[str, Any]: - """Convert this history to JSON-compatible dict. - - Note, this does not include the workflow ID. - """ - return google.protobuf.json_format.MessageToDict( - temporalio.api.history.v1.History(events=self.events) - ) - - -@dataclass -class WorkflowHistoryEventAsyncIterator: - """Asynchronous iterator for history events of a workflow. - - Most users should use ``async for`` on this iterator and not call any of the - methods within. - """ - - def __init__( - self, - client: Client, - input: FetchWorkflowHistoryEventsInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`WorkflowHandle.fetch_history_events`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: ( - None | (Sequence[temporalio.api.history.v1.HistoryEvent]) - ) = None - self._current_page_index = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page( - self, - ) -> Sequence[temporalio.api.history.v1.HistoryEvent] | None: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> bytes | None: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: int | None = None) -> None: # type:ignore[reportUnusedParameter] # https://github.com/temporalio/sdk-python/issues/1239 - """Fetch the next page if any. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - resp = await self._client.workflow_service.get_workflow_execution_history( - temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest( - namespace=self._client.namespace, - execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=self._input.id, - run_id=self._input.run_id or "", - ), - maximum_page_size=page_size or self._input.page_size or 0, - next_page_token=self._next_page_token or b"", - wait_new_event=self._input.wait_new_event, - history_event_filter_type=temporalio.api.enums.v1.HistoryEventFilterType.ValueType( - self._input.event_filter_type - ), - skip_archival=self._input.skip_archival, - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - # We don't support raw history - assert len(resp.raw_history) == 0 - self._current_page = list(resp.history.events) - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> WorkflowHistoryEventAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> temporalio.api.history.v1.HistoryEvent: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Increment page index and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - return ret - - -class ScheduleHandle: - """Handle for interacting with a schedule. - - This is usually created via :py:meth:`Client.get_schedule_handle` or - returned from :py:meth:`Client.create_schedule`. - - Attributes: - id: ID of the schedule. - """ - - def __init__(self, client: Client, id: str) -> None: - """Create schedule handle.""" - self._client = client - self.id = id - - async def backfill( - self, - *backfill: ScheduleBackfill, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Backfill the schedule by going through the specified time periods as - if they passed right now. - - Args: - backfill: Backfill periods. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - if not backfill: - raise ValueError("At least one backfill required") - await self._client._impl.backfill_schedule( - BackfillScheduleInput( - id=self.id, - backfills=backfill, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def delete( - self, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Delete this schedule. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.delete_schedule( - DeleteScheduleInput( - id=self.id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def describe( - self, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> ScheduleDescription: - """Fetch this schedule's description. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - return await self._client._impl.describe_schedule( - DescribeScheduleInput( - id=self.id, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def pause( - self, - *, - note: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Pause the schedule and set a note. - - Args: - note: Note to set on the schedule. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.pause_schedule( - PauseScheduleInput( - id=self.id, - note=note, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def trigger( - self, - *, - overlap: ScheduleOverlapPolicy | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Trigger an action on this schedule to happen immediately. - - Args: - overlap: If set, overrides the schedule's overlap policy. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.trigger_schedule( - TriggerScheduleInput( - id=self.id, - overlap=overlap, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - async def unpause( - self, - *, - note: str | None = None, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Unpause the schedule and set a note. - - Args: - note: Note to set on the schedule. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for the RPC call. - """ - await self._client._impl.unpause_schedule( - UnpauseScheduleInput( - id=self.id, - note=note, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - @overload - async def update( - self, - updater: Callable[[ScheduleUpdateInput], ScheduleUpdate | None], - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: ... - - @overload - async def update( - self, - updater: Callable[[ScheduleUpdateInput], Awaitable[ScheduleUpdate | None]], - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: ... - - async def update( - self, - updater: Callable[ - [ScheduleUpdateInput], - ScheduleUpdate | None | Awaitable[ScheduleUpdate | None], - ], - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - """Update a schedule using a callback to build the update from the - description. - - The callback may be invoked multiple times in a conflict-resolution - loop. - - Args: - updater: Callback that returns the update. It accepts a - :py:class:`ScheduleUpdateInput` and returns a - :py:class:`ScheduleUpdate`. If None is returned or an error - occurs, the update is not attempted. This may be called multiple - times. - rpc_metadata: Headers used on the RPC call. Keys here override - client-level RPC metadata keys. This is for every call made - within. - rpc_timeout: Optional RPC deadline to set for the RPC call. This is - for each call made within, not overall. - """ - await self._client._impl.update_schedule( - UpdateScheduleInput( - id=self.id, - updater=updater, - rpc_metadata=rpc_metadata, - rpc_timeout=rpc_timeout, - ), - ) - - -@dataclass -class ScheduleSpec: - """Specification of the times scheduled actions may occur. - - The times are the union of :py:attr:`calendars`, :py:attr:`intervals`, and - :py:attr:`cron_expressions` excluding anything in :py:attr:`skip`. - """ - - calendars: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) - """Calendar-based specification of times.""" - - intervals: Sequence[ScheduleIntervalSpec] = dataclasses.field(default_factory=list) - """Interval-based specification of times.""" - - cron_expressions: Sequence[str] = dataclasses.field(default_factory=list) - """Cron-based specification of times. - - This is provided for easy migration from legacy string-based cron - scheduling. New uses should use :py:attr:`calendars` instead. These - expressions will be translated to calendar-based specifications on the - server. - """ - - skip: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) - """Set of matching calendar times that will be skipped.""" - - start_at: datetime | None = None - """Time before which any matching times will be skipped.""" - - end_at: datetime | None = None - """Time after which any matching times will be skipped.""" - - jitter: timedelta | None = None - """Jitter to apply each action. - - An action's scheduled time will be incremented by a random value between 0 - and this value if present (but not past the next schedule). - """ - - time_zone_name: str | None = None - """IANA time zone name, for example ``US/Central``.""" - - @staticmethod - def _from_proto(spec: temporalio.api.schedule.v1.ScheduleSpec) -> ScheduleSpec: - return ScheduleSpec( - calendars=[ - ScheduleCalendarSpec._from_proto(c) for c in spec.structured_calendar - ], - intervals=[ScheduleIntervalSpec._from_proto(i) for i in spec.interval], - cron_expressions=spec.cron_string, - skip=[ - ScheduleCalendarSpec._from_proto(c) - for c in spec.exclude_structured_calendar - ], - start_at=spec.start_time.ToDatetime().replace(tzinfo=timezone.utc) - if spec.HasField("start_time") - else None, - end_at=spec.end_time.ToDatetime().replace(tzinfo=timezone.utc) - if spec.HasField("end_time") - else None, - jitter=spec.jitter.ToTimedelta() if spec.HasField("jitter") else None, - time_zone_name=spec.timezone_name or None, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleSpec: - start_time: google.protobuf.timestamp_pb2.Timestamp | None = None - if self.start_at: - start_time = google.protobuf.timestamp_pb2.Timestamp() - start_time.FromDatetime(self.start_at) - end_time: google.protobuf.timestamp_pb2.Timestamp | None = None - if self.end_at: - end_time = google.protobuf.timestamp_pb2.Timestamp() - end_time.FromDatetime(self.end_at) - jitter: google.protobuf.duration_pb2.Duration | None = None - if self.jitter: - jitter = google.protobuf.duration_pb2.Duration() - jitter.FromTimedelta(self.jitter) - return temporalio.api.schedule.v1.ScheduleSpec( - structured_calendar=[cal._to_proto() for cal in self.calendars], - cron_string=self.cron_expressions, - interval=[i._to_proto() for i in self.intervals], - exclude_structured_calendar=[cal._to_proto() for cal in self.skip], - start_time=start_time, - end_time=end_time, - jitter=jitter, - timezone_name=self.time_zone_name or "", - ) - - -@dataclass(frozen=True) -class ScheduleRange: - """Inclusive range for a schedule match value.""" - - start: int - """Inclusive start of the range.""" - - end: int = 0 - """Inclusive end of the range. - - If unset or less than start, defaults to start. - """ - - step: int = 0 - """ - Step to take between each value. - - Unset or 0 defaults as 1. - """ - - def __post_init__(self): - """Set field defaults.""" - # Class is frozen, so we must setattr bypassing dataclass setattr - if self.end < self.start: - object.__setattr__(self, "end", self.start) - if self.step == 0: - object.__setattr__(self, "step", 1) - - @staticmethod - def _from_protos( - ranges: Sequence[temporalio.api.schedule.v1.Range], - ) -> Sequence[ScheduleRange]: - return tuple(ScheduleRange._from_proto(r) for r in ranges) - - @staticmethod - def _from_proto(range: temporalio.api.schedule.v1.Range) -> ScheduleRange: - return ScheduleRange(start=range.start, end=range.end, step=range.step) - - @staticmethod - def _to_protos( - ranges: Sequence[ScheduleRange], - ) -> Sequence[temporalio.api.schedule.v1.Range]: - return tuple(r._to_proto() for r in ranges) - - def _to_proto(self) -> temporalio.api.schedule.v1.Range: - return temporalio.api.schedule.v1.Range( - start=self.start, end=self.end, step=self.step - ) - - -@dataclass -class ScheduleCalendarSpec: - """Specification relative to calendar time when to run an action. - - 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. - """ - - second: Sequence[ScheduleRange] = (ScheduleRange(0),) - """Second range to match, 0-59. Default matches 0.""" - - minute: Sequence[ScheduleRange] = (ScheduleRange(0),) - """Minute range to match, 0-59. Default matches 0.""" - - hour: Sequence[ScheduleRange] = (ScheduleRange(0),) - """Hour range to match, 0-23. Default matches 0.""" - - day_of_month: Sequence[ScheduleRange] = (ScheduleRange(1, 31),) - """Day of month range to match, 1-31. Default matches all days.""" - - month: Sequence[ScheduleRange] = (ScheduleRange(1, 12),) - """Month range to match, 1-12. Default matches all months.""" - - year: Sequence[ScheduleRange] = () - """Optional year range to match. Default of empty matches all years.""" - - day_of_week: Sequence[ScheduleRange] = (ScheduleRange(0, 6),) - """Day of week range to match, 0-6, 0 is Sunday. Default matches all - days.""" - - comment: str | None = None - """Description of this schedule.""" - - @staticmethod - def _from_proto( - spec: temporalio.api.schedule.v1.StructuredCalendarSpec, - ) -> ScheduleCalendarSpec: - return ScheduleCalendarSpec( - second=ScheduleRange._from_protos(spec.second), - minute=ScheduleRange._from_protos(spec.minute), - hour=ScheduleRange._from_protos(spec.hour), - day_of_month=ScheduleRange._from_protos(spec.day_of_month), - month=ScheduleRange._from_protos(spec.month), - year=ScheduleRange._from_protos(spec.year), - day_of_week=ScheduleRange._from_protos(spec.day_of_week), - comment=spec.comment or None, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.StructuredCalendarSpec: - return temporalio.api.schedule.v1.StructuredCalendarSpec( - second=ScheduleRange._to_protos(self.second), - minute=ScheduleRange._to_protos(self.minute), - hour=ScheduleRange._to_protos(self.hour), - day_of_month=ScheduleRange._to_protos(self.day_of_month), - month=ScheduleRange._to_protos(self.month), - year=ScheduleRange._to_protos(self.year), - day_of_week=ScheduleRange._to_protos(self.day_of_week), - comment=self.comment or "", - ) - - -@dataclass -class ScheduleIntervalSpec: - """Specification for scheduling on an interval. - - Matches times expressed as epoch + (n * every) + offset. - """ - - every: timedelta - """Period to repeat the interval.""" - - offset: timedelta | None = None - """Fixed offset added to each interval period.""" - - @staticmethod - def _from_proto( - spec: temporalio.api.schedule.v1.IntervalSpec, - ) -> ScheduleIntervalSpec: - return ScheduleIntervalSpec( - every=spec.interval.ToTimedelta(), - offset=spec.phase.ToTimedelta() if spec.HasField("phase") else None, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.IntervalSpec: - interval = google.protobuf.duration_pb2.Duration() - interval.FromTimedelta(self.every) - phase: google.protobuf.duration_pb2.Duration | None = None - if self.offset: - phase = google.protobuf.duration_pb2.Duration() - phase.FromTimedelta(self.offset) - return temporalio.api.schedule.v1.IntervalSpec(interval=interval, phase=phase) - - -class ScheduleAction(ABC): - """Base class for an action a schedule can take. - - See :py:class:`ScheduleActionStartWorkflow` for the most commonly used - implementation. - """ - - @staticmethod - def _from_proto( - action: temporalio.api.schedule.v1.ScheduleAction, - ) -> ScheduleAction: - if action.HasField("start_workflow"): - return ScheduleActionStartWorkflow._from_proto(action.start_workflow) - else: - raise ValueError(f"Unsupported action: {action.WhichOneof('action')}") - - @abstractmethod - async def _to_proto( - self, client: Client - ) -> temporalio.api.schedule.v1.ScheduleAction: ... - - -@dataclass -class ScheduleActionStartWorkflow(ScheduleAction): - """Schedule action to start a workflow.""" - - workflow: str - args: Sequence[Any] | Sequence[temporalio.api.common.v1.Payload] - id: str - task_queue: str - execution_timeout: timedelta | None - run_timeout: timedelta | None - task_timeout: timedelta | None - retry_policy: temporalio.common.RetryPolicy | None - memo: None | (Mapping[str, Any] | Mapping[str, temporalio.api.common.v1.Payload]) - typed_search_attributes: temporalio.common.TypedSearchAttributes - untyped_search_attributes: temporalio.common.SearchAttributes - """This is deprecated and is only present in case existing untyped - attributes already exist for update. This should never be used when - creating.""" - static_summary: str | temporalio.api.common.v1.Payload | None - static_details: str | temporalio.api.common.v1.Payload | None - priority: temporalio.common.Priority - - headers: Mapping[str, temporalio.api.common.v1.Payload] | None - """ - Headers may still be encoded by the payload codec if present. - """ - _from_raw: bool = dataclasses.field(compare=False, init=False) - - @staticmethod - def _from_proto( # pyright: ignore - info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, # type: ignore[override] - ) -> ScheduleActionStartWorkflow: - return ScheduleActionStartWorkflow("", raw_info=info) - - # Overload for no-param workflow - @overload - def __init__( - self, - workflow: MethodAsyncNoParam[SelfType, ReturnType], - *, - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - retry_policy: temporalio.common.RetryPolicy | None = None, - memo: Mapping[str, Any] | None = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: str | None = None, - static_details: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for single-param workflow - @overload - def __init__( - self, - workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], - arg: ParamType, - *, - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - retry_policy: temporalio.common.RetryPolicy | None = None, - memo: Mapping[str, Any] | None = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: str | None = None, - static_details: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for multi-param workflow - @overload - def __init__( - self, - workflow: Callable[ - Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] - ], - *, - args: Sequence[Any], - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - retry_policy: temporalio.common.RetryPolicy | None = None, - memo: Mapping[str, Any] | None = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: str | None = None, - static_details: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for string-name workflow - @overload - def __init__( - self, - workflow: str, - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str, - task_queue: str, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - retry_policy: temporalio.common.RetryPolicy | None = None, - memo: Mapping[str, Any] | None = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - static_summary: str | None = None, - static_details: str | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: ... - - # Overload for raw info - @overload - def __init__( - self, - workflow: str, - *, - raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, - ) -> None: ... - - def __init__( - self, - workflow: str | Callable[..., Awaitable[Any]], - arg: Any = temporalio.common._arg_unset, - *, - args: Sequence[Any] = [], - id: str | None = None, - task_queue: str | None = None, - execution_timeout: timedelta | None = None, - run_timeout: timedelta | None = None, - task_timeout: timedelta | None = None, - retry_policy: temporalio.common.RetryPolicy | None = None, - memo: Mapping[str, Any] | None = None, - typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, - untyped_search_attributes: temporalio.common.SearchAttributes = {}, - static_summary: str | None = None, - static_details: str | None = None, - headers: Mapping[str, temporalio.api.common.v1.Payload] | None = None, - raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo | None = None, - priority: temporalio.common.Priority = temporalio.common.Priority.default, - ) -> None: - """Create a start-workflow action. - - See :py:meth:`Client.start_workflow` for details on these parameter - values. - """ - super().__init__() - if raw_info: - self._from_raw = True - # Ignore other fields - self.workflow = raw_info.workflow_type.name - self.args = raw_info.input.payloads if raw_info.input else [] - self.id = raw_info.workflow_id - self.task_queue = raw_info.task_queue.name - self.execution_timeout = ( - raw_info.workflow_execution_timeout.ToTimedelta() - if raw_info.HasField("workflow_execution_timeout") - else None - ) - self.run_timeout = ( - raw_info.workflow_run_timeout.ToTimedelta() - if raw_info.HasField("workflow_run_timeout") - else None - ) - self.task_timeout = ( - raw_info.workflow_task_timeout.ToTimedelta() - if raw_info.HasField("workflow_task_timeout") - else None - ) - self.retry_policy = ( - temporalio.common.RetryPolicy.from_proto(raw_info.retry_policy) - if raw_info.HasField("retry_policy") - else None - ) - self.memo = raw_info.memo.fields if raw_info.memo.fields else None - self.typed_search_attributes = ( - temporalio.converter.decode_typed_search_attributes( - raw_info.search_attributes - ) - ) - self.headers = raw_info.header.fields if raw_info.header.fields else None - # Also set the untyped attributes as the set of attributes from - # decode with the typed ones removed - self.untyped_search_attributes = ( - temporalio.converter.decode_search_attributes( - raw_info.search_attributes - ) - ) - for pair in self.typed_search_attributes: - if pair.key.name in self.untyped_search_attributes: - # We know this is mutable here - del self.untyped_search_attributes[pair.key.name] # type: ignore - self.static_summary = ( - raw_info.user_metadata.summary - if raw_info.HasField("user_metadata") and raw_info.user_metadata.summary - else None - ) - self.static_details = ( - raw_info.user_metadata.details - if raw_info.HasField("user_metadata") and raw_info.user_metadata.details - else None - ) - self.priority = ( - temporalio.common.Priority._from_proto(raw_info.priority) - if raw_info.HasField("priority") and raw_info.priority - else temporalio.common.Priority.default - ) - else: - self._from_raw = False - if not id: - raise ValueError("ID required") - if not task_queue: - raise ValueError("Task queue required") - # Use definition if callable - if callable(workflow): - defn = temporalio.workflow._Definition.must_from_run_fn(workflow) - if not defn.name: - raise ValueError("Cannot schedule dynamic workflow explicitly") - workflow = defn.name - elif not isinstance(workflow, str): - raise TypeError("Workflow must be a string or callable") # type:ignore[reportUnreachable] - self.workflow = workflow - self.args = temporalio.common._arg_or_args(arg, args) - self.id = id - self.task_queue = task_queue - self.execution_timeout = execution_timeout - self.run_timeout = run_timeout - self.task_timeout = task_timeout - self.retry_policy = retry_policy - self.memo = memo - self.typed_search_attributes = typed_search_attributes - self.untyped_search_attributes = untyped_search_attributes - self.headers = headers # encode here - self.static_summary = static_summary - self.static_details = static_details - self.priority = priority - - async def _to_proto( - self, client: Client - ) -> temporalio.api.schedule.v1.ScheduleAction: - execution_timeout: google.protobuf.duration_pb2.Duration | None = None - if self.execution_timeout: - execution_timeout = google.protobuf.duration_pb2.Duration() - execution_timeout.FromTimedelta(self.execution_timeout) - run_timeout: google.protobuf.duration_pb2.Duration | None = None - if self.run_timeout: - run_timeout = google.protobuf.duration_pb2.Duration() - run_timeout.FromTimedelta(self.run_timeout) - task_timeout: google.protobuf.duration_pb2.Duration | None = None - if self.task_timeout: - task_timeout = google.protobuf.duration_pb2.Duration() - task_timeout.FromTimedelta(self.task_timeout) - retry_policy: temporalio.api.common.v1.RetryPolicy | None = None - if self.retry_policy: - retry_policy = temporalio.api.common.v1.RetryPolicy() - self.retry_policy.apply_to_proto(retry_policy) - priority: temporalio.api.common.v1.Priority | None = None - if self.priority: - priority = self.priority._to_proto() - data_converter = client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=client.namespace, - workflow_id=self.id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=self.id, type=self.workflow, namespace=client.namespace - ), - ), - ) - action = temporalio.api.schedule.v1.ScheduleAction( - start_workflow=temporalio.api.workflow.v1.NewWorkflowExecutionInfo( - workflow_id=self.id, - workflow_type=temporalio.api.common.v1.WorkflowType(name=self.workflow), - task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=self.task_queue), - input=( - temporalio.api.common.v1.Payloads( - payloads=[ - a - if isinstance(a, temporalio.api.common.v1.Payload) - else (await data_converter.encode([a]))[0] - for a in self.args - ] - ) - if self.args - else None - ), - workflow_execution_timeout=execution_timeout, - workflow_run_timeout=run_timeout, - workflow_task_timeout=task_timeout, - retry_policy=retry_policy, - memo=await data_converter._encode_memo(self.memo) - if self.memo - else None, - user_metadata=await _encode_user_metadata( - data_converter, self.static_summary, self.static_details - ), - priority=priority, - ), - ) - # Add any untyped attributes that are not also in the typed set - untyped_not_in_typed = { - k: v - for k, v in self.untyped_search_attributes.items() - if k not in self.typed_search_attributes - } - if untyped_not_in_typed: - temporalio.converter.encode_search_attributes( - untyped_not_in_typed, action.start_workflow.search_attributes - ) - # TODO (dan): confirm whether this be `is not None` - if self.typed_search_attributes: - temporalio.converter.encode_search_attributes( - self.typed_search_attributes, - action.start_workflow.search_attributes, - ) - if self.headers: - await _apply_headers( - self.headers, - action.start_workflow.header.fields, - client.config(active_config=True)["header_codec_behavior"] - == HeaderCodecBehavior.CODEC - and not self._from_raw, - client.data_converter, - ) - return action - - -class ScheduleOverlapPolicy(IntEnum): - """Controls what happens when a workflow would be started by a schedule but - one is already running. - """ - - SKIP = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP - ) - """Don't start anything. - - When the workflow completes, the next scheduled event after that time will - be considered. - """ - - BUFFER_ONE = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE - ) - """Start the workflow again soon as the current one completes, but only - buffer one start in this way. - - If another start is supposed to happen when the workflow is running, and one - is already buffered, then only the first one will be started after the - running workflow finishes. - """ - - BUFFER_ALL = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ALL - ) - """Buffer up any number of starts to all happen sequentially, immediately - after the running workflow completes.""" - - CANCEL_OTHER = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER - ) - """If there is another workflow running, cancel it, and start the new one - after the old one completes cancellation.""" - - TERMINATE_OTHER = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER - ) - """If there is another workflow running, terminate it and start the new one - immediately.""" - - ALLOW_ALL = int( - temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL - ) - """Start any number of concurrent workflows. - - Note that with this policy, last completion result and last failure will not - be available since workflows are not sequential.""" - - -@dataclass -class ScheduleBackfill: - """Time period and policy for actions taken as if the time passed right - now. - """ - - start_at: datetime - """Start of the range to evaluate the schedule in. - - This is exclusive - """ - end_at: datetime - overlap: ScheduleOverlapPolicy | None = None - - def _to_proto(self) -> temporalio.api.schedule.v1.BackfillRequest: - start_time = google.protobuf.timestamp_pb2.Timestamp() - start_time.FromDatetime(self.start_at) - end_time = google.protobuf.timestamp_pb2.Timestamp() - end_time.FromDatetime(self.end_at) - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED - if self.overlap: - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - self.overlap - ) - return temporalio.api.schedule.v1.BackfillRequest( - start_time=start_time, - end_time=end_time, - overlap_policy=overlap_policy, - ) - - -@dataclass -class SchedulePolicy: - """Policies of a schedule.""" - - overlap: ScheduleOverlapPolicy = dataclasses.field( - default_factory=lambda: ScheduleOverlapPolicy.SKIP - ) - """Controls what happens when an action is started while another is still - running.""" - - catchup_window: timedelta = timedelta(days=365) - """After a Temporal server is unavailable, amount of time in the past to - execute missed actions.""" - - pause_on_failure: bool = False - """Whether to pause the schedule if an action fails or times out. - - Note: For workflows, this only applies after all retries have been - exhausted. - """ - - @staticmethod - def _from_proto(pol: temporalio.api.schedule.v1.SchedulePolicies) -> SchedulePolicy: - return SchedulePolicy( - overlap=ScheduleOverlapPolicy(int(pol.overlap_policy)), - catchup_window=pol.catchup_window.ToTimedelta(), - pause_on_failure=pol.pause_on_failure, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.SchedulePolicies: - catchup_window = google.protobuf.duration_pb2.Duration() - catchup_window.FromTimedelta(self.catchup_window) - return temporalio.api.schedule.v1.SchedulePolicies( - overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - self.overlap - ), - catchup_window=catchup_window, - pause_on_failure=self.pause_on_failure, - ) - - -@dataclass -class ScheduleState: - """State of a schedule.""" - - note: str | None = None - """Human readable message for the schedule. - - The system may overwrite this value on certain conditions like - pause-on-failure. - """ - - paused: bool = False - """Whether the schedule is paused.""" - - # Cannot be set to True on create - limited_actions: bool = False - """ - If true, remaining actions will be decremented for each action taken. - - 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 - """Actions remaining on this schedule. - - Once this number hits 0, no further actions are scheduled automatically. - """ - - @staticmethod - def _from_proto(state: temporalio.api.schedule.v1.ScheduleState) -> ScheduleState: - return ScheduleState( - note=state.notes or None, - paused=state.paused, - limited_actions=state.limited_actions, - remaining_actions=state.remaining_actions, - ) - - def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleState: - return temporalio.api.schedule.v1.ScheduleState( - notes=self.note or "", - paused=self.paused, - limited_actions=self.limited_actions, - remaining_actions=self.remaining_actions, - ) - - -@dataclass -class Schedule: - """A schedule for periodically running an action.""" - - action: ScheduleAction - """Action taken when scheduled.""" - - spec: ScheduleSpec - """When the action is taken.""" - - policy: SchedulePolicy = dataclasses.field(default_factory=SchedulePolicy) - """Schedule policies.""" - - state: ScheduleState = dataclasses.field(default_factory=ScheduleState) - """State of the schedule.""" - - @staticmethod - def _from_proto(sched: temporalio.api.schedule.v1.Schedule) -> Schedule: - return Schedule( - action=ScheduleAction._from_proto(sched.action), - spec=ScheduleSpec._from_proto(sched.spec), - policy=SchedulePolicy._from_proto(sched.policies), - state=ScheduleState._from_proto(sched.state), - ) - - async def _to_proto(self, client: Client) -> temporalio.api.schedule.v1.Schedule: - catchup_window = google.protobuf.duration_pb2.Duration() - catchup_window.FromTimedelta(self.policy.catchup_window) - return temporalio.api.schedule.v1.Schedule( - spec=self.spec._to_proto(), - action=await self.action._to_proto(client), - policies=self.policy._to_proto(), - state=self.state._to_proto(), - ) - - -@dataclass -class ScheduleDescription: - """Description of a schedule.""" - - id: str - """ID of the schedule.""" - - schedule: Schedule - """Schedule details that can be mutated.""" - - info: ScheduleInfo - """Information about the schedule.""" - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Search attributes on the schedule.""" - - search_attributes: temporalio.common.SearchAttributes - """Search attributes on the schedule. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - data_converter: temporalio.converter.DataConverter - """Data converter used for memo decoding.""" - - raw_description: temporalio.api.workflowservice.v1.DescribeScheduleResponse - """Raw description of the schedule.""" - - @staticmethod - def _from_proto( - id: str, - desc: temporalio.api.workflowservice.v1.DescribeScheduleResponse, - converter: temporalio.converter.DataConverter, - ) -> ScheduleDescription: - return ScheduleDescription( - id=id, - schedule=Schedule._from_proto(desc.schedule), - info=ScheduleInfo._from_proto(desc.info), - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - desc.search_attributes - ), - search_attributes=temporalio.converter.decode_search_attributes( - desc.search_attributes - ), - data_converter=converter, - raw_description=desc, - ) - - async def memo(self) -> Mapping[str, Any]: - """Schedule's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come - back. For example, if the memo was originally created with a dataclass, - the value will be a dict. To convert using proper type hints, use - :py:meth:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return await self.data_converter._decode_memo(self.raw_description.memo) - - @overload - async def memo_value( - self, key: str, default: Any = temporalio.common._arg_unset - ) -> Any: ... - - @overload - async def memo_value( - self, key: str, *, type_hint: type[ParamType] - ) -> ParamType: ... - - @overload - async def memo_value( - self, key: str, default: AnyType, *, type_hint: type[ParamType] - ) -> AnyType | ParamType: ... - - async def memo_value( - self, - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: type | None = None, - ) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - return await self.data_converter._decode_memo_field( - self.raw_description.memo, key, default, type_hint - ) - - -@dataclass -class ScheduleInfo: - """Information about a schedule.""" - - num_actions: int - """Number of actions taken by this schedule.""" - - num_actions_missed_catchup_window: int - """Number of times an action was skipped due to missing the catchup - window.""" - - num_actions_skipped_overlap: int - """Number of actions skipped due to overlap.""" - - running_actions: Sequence[ScheduleActionExecution] - """Currently running actions.""" - - recent_actions: Sequence[ScheduleActionResult] - """10 most recent actions, oldest first.""" - - next_action_times: Sequence[datetime] - """Next 10 scheduled action times.""" - - created_at: datetime - """When the schedule was created.""" - - last_updated_at: datetime | None - """When the schedule was last updated.""" - - @staticmethod - def _from_proto(info: temporalio.api.schedule.v1.ScheduleInfo) -> ScheduleInfo: - return ScheduleInfo( - num_actions=info.action_count, - num_actions_missed_catchup_window=info.missed_catchup_window, - num_actions_skipped_overlap=info.overlap_skipped, - running_actions=[ - ScheduleActionExecutionStartWorkflow._from_proto(r) - for r in info.running_workflows - ], - recent_actions=[ - ScheduleActionResult._from_proto(r) for r in info.recent_actions - ], - next_action_times=[ - f.ToDatetime().replace(tzinfo=timezone.utc) - for f in info.future_action_times - ], - created_at=info.create_time.ToDatetime().replace(tzinfo=timezone.utc), - last_updated_at=info.update_time.ToDatetime().replace(tzinfo=timezone.utc) - if info.HasField("update_time") - else None, - ) - - -class ScheduleActionExecution(ABC): - """Base class for an action execution.""" - - pass - - -@dataclass -class ScheduleActionExecutionStartWorkflow(ScheduleActionExecution): - """Execution of a scheduled workflow start.""" - - workflow_id: str - """Workflow ID.""" - - first_execution_run_id: str - """Workflow run ID.""" - - @staticmethod - def _from_proto( - exec: temporalio.api.common.v1.WorkflowExecution, - ) -> ScheduleActionExecutionStartWorkflow: - return ScheduleActionExecutionStartWorkflow( - workflow_id=exec.workflow_id, - first_execution_run_id=exec.run_id, - ) - - -@dataclass -class ScheduleActionResult: - """Information about when an action took place.""" - - scheduled_at: datetime - """Scheduled time of the action including jitter.""" - - started_at: datetime - """When the action actually started.""" - - action: ScheduleActionExecution - """Action that took place.""" - - @staticmethod - def _from_proto( - res: temporalio.api.schedule.v1.ScheduleActionResult, - ) -> ScheduleActionResult: - return ScheduleActionResult( - scheduled_at=res.schedule_time.ToDatetime().replace(tzinfo=timezone.utc), - started_at=res.actual_time.ToDatetime().replace(tzinfo=timezone.utc), - action=ScheduleActionExecutionStartWorkflow._from_proto( - res.start_workflow_result - ), - ) - - -@dataclass -class ScheduleUpdateInput: - """Parameter for an update callback for :py:meth:`ScheduleHandle.update`.""" - - description: ScheduleDescription - """Current description of the schedule.""" - - -@dataclass -class ScheduleUpdate: - """Result of an update callback for :py:meth:`ScheduleHandle.update`.""" - - schedule: Schedule - """Schedule to update.""" - - search_attributes: temporalio.common.TypedSearchAttributes | None = None - """Search attributes to update.""" - - -@dataclass -class ScheduleListDescription: - """Description of a listed schedule.""" - - id: str - """ID of the schedule.""" - - schedule: ScheduleListSchedule | None - """Schedule details that can be mutated. - - This may not be present in older Temporal servers without advanced - visibility. - """ - - info: ScheduleListInfo | None - """Information about the schedule. - - This may not be present in older Temporal servers without advanced - visibility. - """ - - typed_search_attributes: temporalio.common.TypedSearchAttributes - """Search attributes on the schedule.""" - - search_attributes: temporalio.common.SearchAttributes - """Search attributes on the schedule. - - .. deprecated:: - Use :py:attr:`typed_search_attributes` instead. - """ - - data_converter: temporalio.converter.DataConverter - """Data converter used for memo decoding.""" - - raw_entry: temporalio.api.schedule.v1.ScheduleListEntry - """Raw description of the schedule.""" - - @staticmethod - def _from_proto( - entry: temporalio.api.schedule.v1.ScheduleListEntry, - converter: temporalio.converter.DataConverter, - ) -> ScheduleListDescription: - return ScheduleListDescription( - id=entry.schedule_id, - schedule=ScheduleListSchedule._from_proto(entry.info) - if entry.HasField("info") - else None, - info=ScheduleListInfo._from_proto(entry.info) - if entry.HasField("info") - else None, - typed_search_attributes=temporalio.converter.decode_typed_search_attributes( - entry.search_attributes - ), - search_attributes=temporalio.converter.decode_search_attributes( - entry.search_attributes - ), - data_converter=converter, - raw_entry=entry, - ) - - async def memo(self) -> Mapping[str, Any]: - """Schedule's memo values, converted without type hints. - - Since type hints are not used, the default converted values will come - back. For example, if the memo was originally created with a dataclass, - the value will be a dict. To convert using proper type hints, use - :py:meth:`memo_value`. - - Returns: - Mapping of all memo keys and they values without type hints. - """ - return await self.data_converter._decode_memo(self.raw_entry.memo) - - @overload - async def memo_value( - self, key: str, default: Any = temporalio.common._arg_unset - ) -> Any: ... - - @overload - async def memo_value( - self, key: str, *, type_hint: type[ParamType] - ) -> ParamType: ... - - @overload - async def memo_value( - self, key: str, default: AnyType, *, type_hint: type[ParamType] - ) -> AnyType | ParamType: ... - - async def memo_value( - self, - key: str, - default: Any = temporalio.common._arg_unset, - *, - type_hint: type | None = None, - ) -> Any: - """Memo value for the given key, optional default, and optional type - hint. - - Args: - key: Key to get memo value for. - default: Default to use if key is not present. If unset, a - :py:class:`KeyError` is raised when the key does not exist. - type_hint: type hint to use when converting. - - Returns: - Memo value, converted with the type hint if present. - - Raises: - KeyError: Key not present and default not set. - """ - return await self.data_converter._decode_memo_field( - self.raw_entry.memo, key, default, type_hint - ) - - -@dataclass -class ScheduleListSchedule: - """Details for a listed schedule.""" - - action: ScheduleListAction - """Action taken when scheduled.""" - - spec: ScheduleSpec - """When the action is taken.""" - - state: ScheduleListState - """State of the schedule.""" - - @staticmethod - def _from_proto( - info: temporalio.api.schedule.v1.ScheduleListInfo, - ) -> ScheduleListSchedule: - # Only start workflow supported for now - if not info.HasField("workflow_type"): - raise ValueError("Unknown action on schedule") - return ScheduleListSchedule( - action=ScheduleListActionStartWorkflow(workflow=info.workflow_type.name), - spec=ScheduleSpec._from_proto(info.spec), - state=ScheduleListState._from_proto(info), - ) - - -class ScheduleListAction(ABC): - """Base class for an action a listed schedule can take.""" - - pass - - -@dataclass -class ScheduleListActionStartWorkflow(ScheduleListAction): - """Action to start a workflow on a listed schedule.""" - - workflow: str - """Workflow type name.""" - - -@dataclass -class ScheduleListInfo: - """Information about a listed schedule.""" - - recent_actions: Sequence[ScheduleActionResult] - """Most recent actions, oldest first. - - This may be a smaller amount than present on - :py:attr:`ScheduleDescription.info`. - """ - - next_action_times: Sequence[datetime] - """Next scheduled action times. - - This may be a smaller amount than present on - :py:attr:`ScheduleDescription.info`. - """ - - @staticmethod - def _from_proto( - info: temporalio.api.schedule.v1.ScheduleListInfo, - ) -> ScheduleListInfo: - return ScheduleListInfo( - recent_actions=[ - ScheduleActionResult._from_proto(r) for r in info.recent_actions - ], - next_action_times=[ - f.ToDatetime().replace(tzinfo=timezone.utc) - for f in info.future_action_times - ], - ) - - -@dataclass -class ScheduleListState: - """State of a listed schedule.""" - - note: str | None - """Human readable message for the schedule. - - The system may overwrite this value on certain conditions like - pause-on-failure. - """ - - paused: bool - """Whether the schedule is paused.""" - - @staticmethod - def _from_proto( - info: temporalio.api.schedule.v1.ScheduleListInfo, - ) -> ScheduleListState: - return ScheduleListState( - note=info.notes or None, - paused=info.paused, - ) - - -class ScheduleAsyncIterator: - """Asynchronous iterator for :py:class:`ScheduleListDescription` values. - - Most users should use ``async for`` on this iterator and not call any of the - methods within. - """ - - def __init__( - self, - client: Client, - input: ListSchedulesInput, - ) -> None: - """Create an asynchronous iterator for the given input. - - Users should not create this directly, but rather use - :py:meth:`Client.list_schedules`. - """ - self._client = client - self._input = input - self._next_page_token = input.next_page_token - self._current_page: Sequence[ScheduleListDescription] | None = None - self._current_page_index = 0 - - @property - def current_page_index(self) -> int: - """Index of the entry in the current page that will be returned from - the next :py:meth:`__anext__` call. - """ - return self._current_page_index - - @property - def current_page(self) -> Sequence[ScheduleListDescription] | None: - """Current page, if it has been fetched yet.""" - return self._current_page - - @property - def next_page_token(self) -> bytes | None: - """Token for the next page request if any.""" - return self._next_page_token - - async def fetch_next_page(self, *, page_size: int | None = None) -> None: - """Fetch the next page if any. - - Args: - page_size: Override the page size this iterator was originally - created with. - """ - resp = await self._client.workflow_service.list_schedules( - temporalio.api.workflowservice.v1.ListSchedulesRequest( - namespace=self._client.namespace, - maximum_page_size=page_size or self._input.page_size, - next_page_token=self._next_page_token or b"", - query=self._input.query or "", - ), - retry=True, - metadata=self._input.rpc_metadata, - timeout=self._input.rpc_timeout, - ) - self._current_page = [ - ScheduleListDescription._from_proto(v, self._client.data_converter) - for v in resp.schedules - ] - self._current_page_index = 0 - self._next_page_token = resp.next_page_token or None - - def __aiter__(self) -> ScheduleAsyncIterator: - """Return self as the iterator.""" - return self - - async def __anext__(self) -> ScheduleListDescription: - """Get the next execution on this iterator, fetching next page if - necessary. - """ - while True: - # No page? fetch and continue - if self._current_page is None: - await self.fetch_next_page() - continue - # No more left in page? - if self._current_page_index >= len(self._current_page): - # If there is a next page token, try to get another page and try - # again - if self._next_page_token is not None: - await self.fetch_next_page() - continue - # No more pages means we're done - raise StopAsyncIteration - # Get current, increment page index, and return - ret = self._current_page[self._current_page_index] - self._current_page_index += 1 - return ret - - -class WorkflowUpdateHandle(Generic[LocalReturnType]): - """Handle for a workflow update execution request.""" - - def __init__( - self, - client: Client, - id: str, - workflow_id: str, - *, - workflow_run_id: str | None = None, - result_type: type | None = None, - known_outcome: temporalio.api.update.v1.Outcome | None = None, - ): - """Create a workflow update handle. - - Users should not create this directly, but rather use - :py:meth:`WorkflowHandle.start_update` or :py:meth:`WorkflowHandle.get_update_handle`. - """ - self._client = client - self._id = id - self._workflow_id = workflow_id - self._workflow_run_id = workflow_run_id - self._result_type = result_type - self._known_outcome = known_outcome - - @functools.cached_property - def _data_converter(self) -> temporalio.converter.DataConverter: - return self._client.data_converter.with_context( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=self.workflow_id, - ) - ) - - @property - def id(self) -> str: - """ID of this Update request.""" - return self._id - - @property - def workflow_id(self) -> str: - """The ID of the Workflow targeted by this Update.""" - return self._workflow_id - - @property - def workflow_run_id(self) -> str | None: - """If specified, the specific run of the Workflow targeted by this Update.""" - return self._workflow_run_id - - async def result( - self, - *, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> LocalReturnType: - """Wait for and return the result of the update. The result may already be known in which case no network call - is made. Otherwise the result will be polled for until it is returned. - - Args: - rpc_metadata: Headers used on the RPC call. Keys here override client-level RPC metadata keys. - rpc_timeout: Optional RPC deadline to set for each RPC call. Note: this is the timeout for each - RPC call while polling, not a timeout for the function as a whole. If an individual RPC times out, - it will be retried until the result is available. - - Raises: - WorkflowUpdateFailedError: If the update failed. - WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out - or was cancelled. This doesn't mean the update itself was timed - out or cancelled. - RPCError: Update result could not be fetched for some other reason. - """ - # Poll until outcome reached - await self._poll_until_outcome( - rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout - ) - - # Convert outcome to failure or value - assert self._known_outcome - if self._known_outcome.HasField("failure"): - raise WorkflowUpdateFailedError( - await self._data_converter.decode_failure(self._known_outcome.failure), - ) - if not self._known_outcome.success.payloads: - return None # type: ignore - type_hints = [self._result_type] if self._result_type else None - results = await self._data_converter.decode( - self._known_outcome.success.payloads, type_hints - ) - if not results: - return None # type: ignore - elif len(results) > 1: - warnings.warn(f"Expected single update result, got {len(results)}") - return results[0] - - async def _poll_until_outcome( - self, - rpc_metadata: Mapping[str, str | bytes] = {}, - rpc_timeout: timedelta | None = None, - ) -> None: - if self._known_outcome: - return - req = temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest( - namespace=self._client.namespace, - update_ref=temporalio.api.update.v1.UpdateRef( - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=self.workflow_id, - run_id=self.workflow_run_id or "", - ), - update_id=self.id, - ), - identity=self._client.identity, - wait_policy=temporalio.api.update.v1.WaitPolicy( - lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED - ), - ) - - # Continue polling as long as we have no outcome - while True: - try: - res = ( - await self._client.workflow_service.poll_workflow_execution_update( - req, - retry=True, - metadata=rpc_metadata, - timeout=rpc_timeout, - ) - ) - if res.HasField("outcome"): - self._known_outcome = res.outcome - return - except RPCError as err: - if ( - err.status == RPCStatusCode.DEADLINE_EXCEEDED - or err.status == RPCStatusCode.CANCELLED - ): - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - else: - raise - except asyncio.CancelledError as err: - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - - -class WorkflowUpdateStage(IntEnum): - """Stage to wait for workflow update to reach before returning from - ``start_update``. - """ - - ADMITTED = int( - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED - ) - ACCEPTED = int( - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED - ) - COMPLETED = int( - temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED - ) - - -class WorkflowFailureError(temporalio.exceptions.TemporalError): - """Error that occurs when a workflow is unsuccessful.""" - - def __init__(self, *, cause: BaseException) -> None: - """Create workflow failure error.""" - super().__init__("Workflow execution failed") - self.__cause__ = cause - - @property - def cause(self) -> BaseException: - """Cause of the workflow failure.""" - assert self.__cause__ - return self.__cause__ - - -class WorkflowContinuedAsNewError(temporalio.exceptions.TemporalError): - """Error that occurs when a workflow was continued as new.""" - - def __init__(self, new_execution_run_id: str) -> None: - """Create workflow continue as new error.""" - super().__init__("Workflow continued as new") - self._new_execution_run_id = new_execution_run_id - - @property - def new_execution_run_id(self) -> str: - """New execution run ID the workflow continued to""" - return self._new_execution_run_id - - -class WorkflowQueryRejectedError(temporalio.exceptions.TemporalError): - """Error that occurs when a query was rejected.""" - - def __init__(self, status: WorkflowExecutionStatus | None) -> None: - """Create workflow query rejected error.""" - super().__init__(f"Query rejected, status: {status}") - self._status = status - - @property - def status(self) -> WorkflowExecutionStatus | None: - """Get workflow execution status causing rejection.""" - return self._status - - -class WorkflowQueryFailedError(temporalio.exceptions.TemporalError): - """Error that occurs when a query fails.""" - - def __init__(self, message: str) -> None: - """Create workflow query failed error.""" - super().__init__(message) - self._message = message - - @property - def message(self) -> str: - """Get query failed message.""" - return self._message - - -class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): - """Error that occurs when an update fails.""" - - def __init__(self, cause: BaseException) -> None: - """Create workflow update failed error.""" - super().__init__("Workflow update failed") - self.__cause__ = cause - - @property - def cause(self) -> BaseException: - """Cause of the update failure.""" - assert self.__cause__ - return self.__cause__ - - -class RPCTimeoutOrCancelledError(temporalio.exceptions.TemporalError): - """Error that occurs on some client calls that timeout or get cancelled.""" - - pass - - -class WorkflowUpdateRPCTimeoutOrCancelledError(RPCTimeoutOrCancelledError): - """Error that occurs when update RPC call times out or is cancelled. - - Note, this is not related to any general concept of timing out or cancelling - a running update, this is only related to the client call itself. - """ - - def __init__(self) -> None: - """Create workflow update timeout or cancelled error.""" - super().__init__("Timeout or cancellation waiting for update") - - -class ActivityFailureError(temporalio.exceptions.TemporalError): - """Error that occurs when an activity is unsuccessful. - - .. warning:: - This API is experimental. - """ - - def __init__(self, *, cause: BaseException) -> None: - """Create activity failure error.""" - super().__init__("Activity execution failed") - self.__cause__ = cause - - @property - def cause(self) -> BaseException: - """Cause of the activity failure.""" - assert self.__cause__ - return self.__cause__ - - -class AsyncActivityCancelledError(temporalio.exceptions.TemporalError): - """Error that occurs when async activity attempted heartbeat but was cancelled.""" - - def __init__(self, details: ActivityCancellationDetails | None = None) -> None: - """Create async activity cancelled error.""" - super().__init__("Activity cancelled") - self.details = details - - -class ScheduleAlreadyRunningError(temporalio.exceptions.TemporalError): - """Error when a schedule is already running.""" - - def __init__(self) -> None: - """Create schedule already running error.""" - super().__init__("Schedule already running") - - -@dataclass -class StartWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.start_workflow`.""" - - workflow: str - args: Sequence[Any] - id: str - task_queue: str - execution_timeout: timedelta | None - run_timeout: timedelta | None - task_timeout: timedelta | None - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy - retry_policy: temporalio.common.RetryPolicy | None - cron_schedule: str - memo: Mapping[str, Any] | None - search_attributes: None | ( - temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes - ) - start_delay: timedelta | None - headers: Mapping[str, temporalio.api.common.v1.Payload] - start_signal: str | None - start_signal_args: Sequence[Any] - static_summary: str | None - static_details: str | None - # Type may be absent - ret_type: type | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - request_eager_start: bool - priority: temporalio.common.Priority - # The following options are experimental and unstable. - callbacks: Sequence[Callback] - workflow_event_links: Sequence[temporalio.api.common.v1.Link.WorkflowEvent] - request_id: str | None - versioning_override: temporalio.common.VersioningOverride | None = None - - -@dataclass -class CancelWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.cancel_workflow`.""" - - id: str - run_id: str | None - first_execution_run_id: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class DescribeWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.describe_workflow`.""" - - id: str - run_id: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class FetchWorkflowHistoryEventsInput: - """Input for :py:meth:`OutboundInterceptor.fetch_workflow_history_events`.""" - - id: str - run_id: str | None - page_size: int | None - next_page_token: bytes | None - wait_new_event: bool - event_filter_type: WorkflowHistoryEventFilterType - skip_archival: bool - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class ListWorkflowsInput: - """Input for :py:meth:`OutboundInterceptor.list_workflows`.""" - - query: str | None - page_size: int - next_page_token: bytes | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - limit: int | None - - -@dataclass -class CountWorkflowsInput: - """Input for :py:meth:`OutboundInterceptor.count_workflows`.""" - - query: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class QueryWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.query_workflow`.""" - - id: str - run_id: str | None - query: str - args: Sequence[Any] - reject_condition: temporalio.common.QueryRejectCondition | None - headers: Mapping[str, temporalio.api.common.v1.Payload] - # Type may be absent - ret_type: type | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class SignalWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.signal_workflow`.""" - - id: str - run_id: str | None - signal: str - args: Sequence[Any] - headers: Mapping[str, temporalio.api.common.v1.Payload] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class TerminateWorkflowInput: - """Input for :py:meth:`OutboundInterceptor.terminate_workflow`.""" - - id: str - run_id: str | None - first_execution_run_id: str | None - args: Sequence[Any] - reason: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class StartActivityInput: - """Input for :py:meth:`OutboundInterceptor.start_activity`. - - .. warning:: - This API is experimental. - """ - - activity_type: str - args: Sequence[Any] - id: str - task_queue: str - result_type: type | None - schedule_to_close_timeout: timedelta | None - start_to_close_timeout: timedelta | None - schedule_to_start_timeout: timedelta | None - heartbeat_timeout: timedelta | None - id_reuse_policy: temporalio.common.ActivityIDReusePolicy - id_conflict_policy: temporalio.common.ActivityIDConflictPolicy - retry_policy: temporalio.common.RetryPolicy | None - priority: temporalio.common.Priority - search_attributes: temporalio.common.TypedSearchAttributes | None - summary: str | None - start_delay: timedelta | None - headers: Mapping[str, temporalio.api.common.v1.Payload] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class CancelActivityInput: - """Input for :py:meth:`OutboundInterceptor.cancel_activity`. - - .. warning:: - This API is experimental. - """ - - activity_id: str - activity_run_id: str | None - reason: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class TerminateActivityInput: - """Input for :py:meth:`OutboundInterceptor.terminate_activity`. - - .. warning:: - This API is experimental. - """ - - activity_id: str - activity_run_id: str | None - reason: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class DescribeActivityInput: - """Input for :py:meth:`OutboundInterceptor.describe_activity`. - - .. warning:: - This API is experimental. - """ - - activity_id: str - activity_run_id: str | None - long_poll_token: bytes | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class ListActivitiesInput: - """Input for :py:meth:`OutboundInterceptor.list_activities`. - - .. warning:: - This API is experimental. - """ - - query: str | None - page_size: int - next_page_token: bytes | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - limit: int | None - - -@dataclass -class CountActivitiesInput: - """Input for :py:meth:`OutboundInterceptor.count_activities`. - - .. warning:: - This API is experimental. - """ - - query: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class StartWorkflowUpdateInput: - """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" - - id: str - run_id: str | None - first_execution_run_id: str | None - update_id: str | None - update: str - args: Sequence[Any] - wait_for_stage: WorkflowUpdateStage - headers: Mapping[str, temporalio.api.common.v1.Payload] - ret_type: type | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class UpdateWithStartUpdateWorkflowInput: - """Update input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" - - update_id: str | None - update: str - args: Sequence[Any] - wait_for_stage: WorkflowUpdateStage - headers: Mapping[str, temporalio.api.common.v1.Payload] - ret_type: type | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class UpdateWithStartStartWorkflowInput: - """StartWorkflow input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" - - # Similar to StartWorkflowInput but without e.g. run_id, start_signal, - # start_signal_args, request_eager_start. - - workflow: str - args: Sequence[Any] - id: str - task_queue: str - execution_timeout: timedelta | None - run_timeout: timedelta | None - task_timeout: timedelta | None - id_reuse_policy: temporalio.common.WorkflowIDReusePolicy - id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy - retry_policy: temporalio.common.RetryPolicy | None - cron_schedule: str - memo: Mapping[str, Any] | None - search_attributes: None | ( - temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes - ) - start_delay: timedelta | None - headers: Mapping[str, temporalio.api.common.v1.Payload] - static_summary: str | None - static_details: str | None - # Type may be absent - ret_type: type | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - priority: temporalio.common.Priority - versioning_override: temporalio.common.VersioningOverride | None = None - - -@dataclass -class StartWorkflowUpdateWithStartInput: - """Input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" - - start_workflow_input: UpdateWithStartStartWorkflowInput - update_workflow_input: UpdateWithStartUpdateWorkflowInput - _on_start: Callable[ - [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None - ] - _on_start_error: Callable[[BaseException], None] - - -@dataclass -class HeartbeatAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.heartbeat_async_activity`.""" - - id_or_token: AsyncActivityIDReference | bytes - details: Sequence[Any] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - data_converter_override: DataConverter | None = None - - -@dataclass -class CompleteAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.complete_async_activity`.""" - - id_or_token: AsyncActivityIDReference | bytes - result: Any | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - data_converter_override: DataConverter | None = None - - -@dataclass -class FailAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.fail_async_activity`.""" - - id_or_token: AsyncActivityIDReference | bytes - error: Exception - last_heartbeat_details: Sequence[Any] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - data_converter_override: DataConverter | None = None - - -@dataclass -class ReportCancellationAsyncActivityInput: - """Input for :py:meth:`OutboundInterceptor.report_cancellation_async_activity`.""" - - id_or_token: AsyncActivityIDReference | bytes - details: Sequence[Any] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - data_converter_override: DataConverter | None = None - - -@dataclass -class CreateScheduleInput: - """Input for :py:meth:`OutboundInterceptor.create_schedule`.""" - - id: str - schedule: Schedule - trigger_immediately: bool - backfill: Sequence[ScheduleBackfill] - memo: Mapping[str, Any] | None - search_attributes: None | ( - temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes - ) - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class ListSchedulesInput: - """Input for :py:meth:`OutboundInterceptor.list_schedules`.""" - - page_size: int - next_page_token: bytes | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - query: str | None = None - - -@dataclass -class BackfillScheduleInput: - """Input for :py:meth:`OutboundInterceptor.backfill_schedule`.""" - - id: str - backfills: Sequence[ScheduleBackfill] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class DeleteScheduleInput: - """Input for :py:meth:`OutboundInterceptor.delete_schedule`.""" - - id: str - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class DescribeScheduleInput: - """Input for :py:meth:`OutboundInterceptor.describe_schedule`.""" - - id: str - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class PauseScheduleInput: - """Input for :py:meth:`OutboundInterceptor.pause_schedule`.""" - - id: str - note: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class TriggerScheduleInput: - """Input for :py:meth:`OutboundInterceptor.trigger_schedule`.""" - - id: str - overlap: ScheduleOverlapPolicy | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class UnpauseScheduleInput: - """Input for :py:meth:`OutboundInterceptor.unpause_schedule`.""" - - id: str - note: str | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class UpdateScheduleInput: - """Input for :py:meth:`OutboundInterceptor.update_schedule`.""" - - id: str - updater: Callable[ - [ScheduleUpdateInput], - ScheduleUpdate | None | Awaitable[ScheduleUpdate | None], - ] - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class UpdateWorkerBuildIdCompatibilityInput: - """Input for :py:meth:`OutboundInterceptor.update_worker_build_id_compatibility`.""" - - task_queue: str - operation: BuildIdOp - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class GetWorkerBuildIdCompatibilityInput: - """Input for :py:meth:`OutboundInterceptor.get_worker_build_id_compatibility`.""" - - task_queue: str - max_sets: int | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class GetWorkerTaskReachabilityInput: - """Input for :py:meth:`OutboundInterceptor.get_worker_task_reachability`.""" - - build_ids: Sequence[str] - task_queues: Sequence[str] - reachability: TaskReachabilityType | None - rpc_metadata: Mapping[str, str | bytes] - rpc_timeout: timedelta | None - - -@dataclass -class Interceptor: - """Interceptor for clients. - - This should be extended by any client interceptors. - """ - - def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: - """Method called for intercepting a client. - - Args: - next: The underlying outbound interceptor this interceptor should - delegate to. - - Returns: - The new interceptor that will be called for each client call. - """ - return next - - -class OutboundInterceptor: - """OutboundInterceptor for intercepting client calls. - - This should be extended by any client outbound interceptors. - """ - - def __init__(self, next: OutboundInterceptor) -> None: - """Create the outbound interceptor. - - Args: - next: The next interceptor in the chain. The default implementation - of all calls is to delegate to the next interceptor. - """ - self.next = next - - ### Workflow calls - - async def start_workflow( - self, input: StartWorkflowInput - ) -> WorkflowHandle[Any, Any]: - """Called for every :py:meth:`Client.start_workflow` call.""" - return await self.next.start_workflow(input) - - async def cancel_workflow(self, input: CancelWorkflowInput) -> None: - """Called for every :py:meth:`WorkflowHandle.cancel` call.""" - await self.next.cancel_workflow(input) - - async def describe_workflow( - self, input: DescribeWorkflowInput - ) -> WorkflowExecutionDescription: - """Called for every :py:meth:`WorkflowHandle.describe` call.""" - return await self.next.describe_workflow(input) - - def fetch_workflow_history_events( - self, input: FetchWorkflowHistoryEventsInput - ) -> WorkflowHistoryEventAsyncIterator: - """Called for every :py:meth:`WorkflowHandle.fetch_history_events` call.""" - return self.next.fetch_workflow_history_events(input) - - def list_workflows( - self, input: ListWorkflowsInput - ) -> WorkflowExecutionAsyncIterator: - """Called for every :py:meth:`Client.list_workflows` call.""" - return self.next.list_workflows(input) - - async def count_workflows( - self, input: CountWorkflowsInput - ) -> WorkflowExecutionCount: - """Called for every :py:meth:`Client.count_workflows` call.""" - return await self.next.count_workflows(input) - - async def query_workflow(self, input: QueryWorkflowInput) -> Any: - """Called for every :py:meth:`WorkflowHandle.query` call.""" - return await self.next.query_workflow(input) - - async def signal_workflow(self, input: SignalWorkflowInput) -> None: - """Called for every :py:meth:`WorkflowHandle.signal` call.""" - await self.next.signal_workflow(input) - - async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: - """Called for every :py:meth:`WorkflowHandle.terminate` call.""" - await self.next.terminate_workflow(input) - - ### Activity calls - - async def start_activity(self, input: StartActivityInput) -> ActivityHandle[Any]: - """Called for every :py:meth:`Client.start_activity` call. - - .. warning:: - This API is experimental. - """ - return await self.next.start_activity(input) - - async def cancel_activity(self, input: CancelActivityInput) -> None: - """Called for every :py:meth:`ActivityHandle.cancel` call. - - .. warning:: - This API is experimental. - """ - await self.next.cancel_activity(input) - - async def terminate_activity(self, input: TerminateActivityInput) -> None: - """Called for every :py:meth:`ActivityHandle.terminate` call. - - .. warning:: - This API is experimental. - """ - await self.next.terminate_activity(input) - - async def describe_activity( - self, input: DescribeActivityInput - ) -> ActivityExecutionDescription: - """Called for every :py:meth:`ActivityHandle.describe` call. - - .. warning:: - This API is experimental. - """ - return await self.next.describe_activity(input) - - def list_activities( - self, input: ListActivitiesInput - ) -> ActivityExecutionAsyncIterator: - """Called for every :py:meth:`Client.list_activities` call. - - .. warning:: - This API is experimental. - """ - return self.next.list_activities(input) - - async def count_activities( - self, input: CountActivitiesInput - ) -> ActivityExecutionCount: - """Called for every :py:meth:`Client.count_activities` call. - - .. warning:: - This API is experimental. - """ - return await self.next.count_activities(input) - - async def start_workflow_update( - self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle[Any]: - """Called for every :py:meth:`WorkflowHandle.start_update` and :py:meth:`WorkflowHandle.execute_update` call.""" - return await self.next.start_workflow_update(input) - - async def start_update_with_start_workflow( - self, input: StartWorkflowUpdateWithStartInput - ) -> WorkflowUpdateHandle[Any]: - """Called for every :py:meth:`Client.start_update_with_start_workflow` and :py:meth:`Client.execute_update_with_start_workflow` call.""" - return await self.next.start_update_with_start_workflow(input) - - ### Async activity calls - - async def heartbeat_async_activity( - self, input: HeartbeatAsyncActivityInput - ) -> None: - """Called for every :py:meth:`AsyncActivityHandle.heartbeat` call.""" - await self.next.heartbeat_async_activity(input) - - async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: - """Called for every :py:meth:`AsyncActivityHandle.complete` call.""" - await self.next.complete_async_activity(input) - - async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: - """Called for every :py:meth:`AsyncActivityHandle.fail` call.""" - await self.next.fail_async_activity(input) - - async def report_cancellation_async_activity( - self, input: ReportCancellationAsyncActivityInput - ) -> None: - """Called for every :py:meth:`AsyncActivityHandle.report_cancellation` call.""" - await self.next.report_cancellation_async_activity(input) - - ### Schedule calls - - async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: - """Called for every :py:meth:`Client.create_schedule` call.""" - return await self.next.create_schedule(input) - - def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: - """Called for every :py:meth:`Client.list_schedules` call.""" - return self.next.list_schedules(input) - - async def backfill_schedule(self, input: BackfillScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.backfill` call.""" - await self.next.backfill_schedule(input) - - async def delete_schedule(self, input: DeleteScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.delete` call.""" - await self.next.delete_schedule(input) - - async def describe_schedule( - self, input: DescribeScheduleInput - ) -> ScheduleDescription: - """Called for every :py:meth:`ScheduleHandle.describe` call.""" - return await self.next.describe_schedule(input) - - async def pause_schedule(self, input: PauseScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.pause` call.""" - await self.next.pause_schedule(input) - - async def trigger_schedule(self, input: TriggerScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.trigger` call.""" - await self.next.trigger_schedule(input) - - async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.unpause` call.""" - await self.next.unpause_schedule(input) - - async def update_schedule(self, input: UpdateScheduleInput) -> None: - """Called for every :py:meth:`ScheduleHandle.update` call.""" - await self.next.update_schedule(input) - - async def update_worker_build_id_compatibility( - self, input: UpdateWorkerBuildIdCompatibilityInput - ) -> None: - """Called for every :py:meth:`Client.update_worker_build_id_compatibility` call.""" - await self.next.update_worker_build_id_compatibility(input) - - async def get_worker_build_id_compatibility( - self, input: GetWorkerBuildIdCompatibilityInput - ) -> WorkerBuildIdVersionSets: - """Called for every :py:meth:`Client.get_worker_build_id_compatibility` call.""" - return await self.next.get_worker_build_id_compatibility(input) - - async def get_worker_task_reachability( - self, input: GetWorkerTaskReachabilityInput - ) -> WorkerTaskReachability: - """Called for every :py:meth:`Client.get_worker_task_reachability` call.""" - return await self.next.get_worker_task_reachability(input) - - -class _ClientImpl(OutboundInterceptor): - def __init__(self, client: Client) -> None: # type: ignore - # We are intentionally not calling the base class's __init__ here - self._client = client - - ### Workflow calls - - async def start_workflow( - self, input: StartWorkflowInput - ) -> WorkflowHandle[Any, Any]: - req: ( - temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest - | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - ) - if input.start_signal is not None: - req = await self._build_signal_with_start_workflow_execution_request(input) - else: - req = await self._build_start_workflow_execution_request(input) - - resp: ( - temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse - | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse - ) - first_execution_run_id = None - eagerly_started = False - try: - if isinstance( - req, - temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, - ): - resp = await self._client.workflow_service.signal_with_start_workflow_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - resp = await self._client.workflow_service.start_workflow_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - first_execution_run_id = resp.run_id - eagerly_started = resp.HasField("eager_workflow_task") - except RPCError as err: - # If the status is ALREADY_EXISTS and the details can be extracted - # as already started, use a different exception - if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: - details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() - if err.grpc_status.details[0].Unpack(details): - raise temporalio.exceptions.WorkflowAlreadyStartedError( - input.id, input.workflow, run_id=details.run_id - ) - raise - handle: WorkflowHandle[Any, Any] = WorkflowHandle( - self._client, - req.workflow_id, - result_run_id=resp.run_id, - first_execution_run_id=first_execution_run_id, - result_type=input.ret_type, - start_workflow_response=resp, - ) - setattr(handle, "__temporal_eagerly_started", eagerly_started) - return handle - - async def _build_start_workflow_execution_request( - self, input: StartWorkflowInput - ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: - req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() - await self._populate_start_workflow_execution_request(req, input) - # _populate_start_workflow_execution_request is used for both StartWorkflowInput - # and UpdateWithStartStartWorkflowInput. UpdateWithStartStartWorkflowInput does - # not have the following two fields so they are handled here. - req.request_eager_execution = input.request_eager_start - if input.request_id: - req.request_id = input.request_id - - links = [ - temporalio.api.common.v1.Link(workflow_event=link) - for link in input.workflow_event_links - ] - req.completion_callbacks.extend( - temporalio.api.common.v1.Callback( - nexus=temporalio.api.common.v1.Callback.Nexus( - url=callback.url, - header=callback.headers, - ), - links=links, - ) - for callback in input.callbacks - ) - # Links are duplicated on request for compatibility with older server versions. - req.links.extend(links) - - if temporalio.nexus._operation_context._in_nexus_backing_workflow_start_context(): - req.on_conflict_options.attach_request_id = True - req.on_conflict_options.attach_completion_callbacks = True - req.on_conflict_options.attach_links = True - - return req - - async def _build_signal_with_start_workflow_execution_request( - self, input: StartWorkflowInput - ) -> temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest: - assert input.start_signal - data_converter = self._client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=input.id, type=input.workflow, namespace=self._client.namespace - ), - ), - ) - req = temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest( - signal_name=input.start_signal - ) - if input.start_signal_args: - req.signal_input.payloads.extend( - await data_converter.encode(input.start_signal_args) - ) - await self._populate_start_workflow_execution_request(req, input) - return req - - async def _build_update_with_start_start_workflow_execution_request( - self, input: UpdateWithStartStartWorkflowInput - ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: - req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() - await self._populate_start_workflow_execution_request(req, input) - return req - - async def _populate_start_workflow_execution_request( - self, - req: ( - temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest - | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest - ), - input: StartWorkflowInput | UpdateWithStartStartWorkflowInput, - ) -> None: - data_converter = self._client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=input.id, type=input.workflow, namespace=self._client.namespace - ), - ), - ) - req.namespace = self._client.namespace - req.workflow_id = input.id - req.workflow_type.name = input.workflow - req.task_queue.name = input.task_queue - if input.args: - req.input.payloads.extend(await data_converter.encode(input.args)) - if input.execution_timeout is not None: - req.workflow_execution_timeout.FromTimedelta(input.execution_timeout) - if input.run_timeout is not None: - req.workflow_run_timeout.FromTimedelta(input.run_timeout) - if input.task_timeout is not None: - req.workflow_task_timeout.FromTimedelta(input.task_timeout) - req.identity = self._client.identity - req.request_id = str(uuid.uuid4()) - req.workflow_id_reuse_policy = cast( - "temporalio.api.enums.v1.WorkflowIdReusePolicy.ValueType", - int(input.id_reuse_policy), - ) - req.workflow_id_conflict_policy = cast( - "temporalio.api.enums.v1.WorkflowIdConflictPolicy.ValueType", - int(input.id_conflict_policy), - ) - - if input.retry_policy is not None: - input.retry_policy.apply_to_proto(req.retry_policy) - req.cron_schedule = input.cron_schedule - if input.memo is not None: - await data_converter._encode_memo_existing(input.memo, req.memo) - if input.search_attributes is not None: - temporalio.converter.encode_search_attributes( - input.search_attributes, req.search_attributes - ) - metadata = await _encode_user_metadata( - data_converter, input.static_summary, input.static_details - ) - if metadata is not None: - req.user_metadata.CopyFrom(metadata) - if input.start_delay is not None: - req.workflow_start_delay.FromTimedelta(input.start_delay) - if input.headers is not None: # type:ignore[reportUnnecessaryComparison] - await self._apply_headers(input.headers, req.header.fields) - if input.priority is not None: # type:ignore[reportUnnecessaryComparison] - req.priority.CopyFrom(input.priority._to_proto()) - if input.versioning_override is not None: - req.versioning_override.CopyFrom(input.versioning_override._to_proto()) - - async def cancel_workflow(self, input: CancelWorkflowInput) -> None: - await self._client.workflow_service.request_cancel_workflow_execution( - temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - first_execution_run_id=input.first_execution_run_id or "", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def describe_workflow( - self, input: DescribeWorkflowInput - ) -> WorkflowExecutionDescription: - return await WorkflowExecutionDescription._from_raw_description( - await self._client.workflow_service.describe_workflow_execution( - temporalio.api.workflowservice.v1.DescribeWorkflowExecutionRequest( - namespace=self._client.namespace, - execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ), - namespace=self._client.namespace, - converter=self._client.data_converter.with_context( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.id, - ) - ), - ) - - def fetch_workflow_history_events( - self, input: FetchWorkflowHistoryEventsInput - ) -> WorkflowHistoryEventAsyncIterator: - return WorkflowHistoryEventAsyncIterator(self._client, input) - - def list_workflows( - self, input: ListWorkflowsInput - ) -> WorkflowExecutionAsyncIterator: - return WorkflowExecutionAsyncIterator(self._client, input) - - async def count_workflows( - self, input: CountWorkflowsInput - ) -> WorkflowExecutionCount: - return WorkflowExecutionCount._from_raw( - await self._client.workflow_service.count_workflow_executions( - temporalio.api.workflowservice.v1.CountWorkflowExecutionsRequest( - namespace=self._client.namespace, - query=input.query or "", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - ) - - async def query_workflow(self, input: QueryWorkflowInput) -> Any: - data_converter = self._client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=input.id, - run_id=input.run_id or None, - namespace=self._client.namespace, - ), - ), - ) - req = temporalio.api.workflowservice.v1.QueryWorkflowRequest( - namespace=self._client.namespace, - execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - ) - if input.reject_condition: - req.query_reject_condition = cast( - "temporalio.api.enums.v1.QueryRejectCondition.ValueType", - int(input.reject_condition), - ) - req.query.query_type = input.query - if input.args: - req.query.query_args.payloads.extend( - await data_converter.encode(input.args) - ) - if input.headers is not None: # type:ignore[reportUnnecessaryComparison] - await self._apply_headers(input.headers, req.query.header.fields) - try: - resp = await self._client.workflow_service.query_workflow( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - except RPCError as err: - # If the status is INVALID_ARGUMENT, we can assume it's a query - # failed error - if err.status == RPCStatusCode.INVALID_ARGUMENT: - raise WorkflowQueryFailedError(err.message) - else: - raise - if resp.HasField("query_rejected"): - raise WorkflowQueryRejectedError( - WorkflowExecutionStatus(resp.query_rejected.status) - if resp.query_rejected.status - else None - ) - if not resp.query_result.payloads: - return None - type_hints = [input.ret_type] if input.ret_type else None - results = await data_converter.decode(resp.query_result.payloads, type_hints) - if not results: - return None - elif len(results) > 1: - warnings.warn(f"Expected single query result, got {len(results)}") - return results[0] - - async def signal_workflow(self, input: SignalWorkflowInput) -> None: - data_converter = self._client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=input.id, - run_id=input.run_id or None, - namespace=self._client.namespace, - ), - ), - ) - req = temporalio.api.workflowservice.v1.SignalWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - signal_name=input.signal, - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ) - if input.args: - req.input.payloads.extend(await data_converter.encode(input.args)) - if input.headers is not None: # type:ignore[reportUnnecessaryComparison] - await self._apply_headers(input.headers, req.header.fields) - await self._client.workflow_service.signal_workflow_execution( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - - async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: - data_converter = self._client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=input.id, - run_id=input.run_id or None, - namespace=self._client.namespace, - ), - ), - ) - req = temporalio.api.workflowservice.v1.TerminateWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=input.id, - run_id=input.run_id or "", - ), - reason=input.reason or "", - identity=self._client.identity, - first_execution_run_id=input.first_execution_run_id or "", - ) - if input.args: - req.details.payloads.extend(await data_converter.encode(input.args)) - await self._client.workflow_service.terminate_workflow_execution( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - - async def start_activity(self, input: StartActivityInput) -> ActivityHandle[Any]: - """Start an activity and return a handle to it.""" - if not (input.start_to_close_timeout or input.schedule_to_close_timeout): - raise ValueError( - "Activity must have start_to_close_timeout or schedule_to_close_timeout" - ) - if input.start_delay is not None and input.start_delay < timedelta(0): - raise ValueError("start_delay must be non-negative") - req = await self._build_start_activity_execution_request(input) - - resp: temporalio.api.workflowservice.v1.StartActivityExecutionResponse - try: - resp = await self._client.workflow_service.start_activity_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - except RPCError as err: - # If the status is ALREADY_EXISTS and the details can be extracted - # as already started, use a different exception - if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: - details = temporalio.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure() - if err.grpc_status.details[0].Unpack(details): - raise temporalio.exceptions.ActivityAlreadyStartedError( - input.id, input.activity_type, run_id=details.run_id - ) - raise - return ActivityHandle( - self._client, - input.id, - run_id=resp.run_id, - result_type=input.result_type, - ) - - async def _build_start_activity_execution_request( - self, input: StartActivityInput - ) -> temporalio.api.workflowservice.v1.StartActivityExecutionRequest: - """Build StartActivityExecutionRequest from input.""" - data_converter = self._client.data_converter._with_contexts( - ActivitySerializationContext( - namespace=self._client.namespace, - activity_id=input.id, - activity_type=input.activity_type, - activity_task_queue=input.task_queue, - is_local=False, - workflow_id=None, - workflow_type=None, - ), - StorageDriverStoreContext( - target=StorageDriverActivityInfo( - id=input.id, - type=input.activity_type, - namespace=self._client.namespace, - ), - ), - ) - - req = temporalio.api.workflowservice.v1.StartActivityExecutionRequest( - namespace=self._client.namespace, - identity=self._client.identity, - activity_id=input.id, - activity_type=temporalio.api.common.v1.ActivityType( - name=input.activity_type - ), - task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=input.task_queue), - id_reuse_policy=cast( - "temporalio.api.enums.v1.ActivityIdReusePolicy.ValueType", - int(input.id_reuse_policy), - ), - id_conflict_policy=cast( - "temporalio.api.enums.v1.ActivityIdConflictPolicy.ValueType", - int(input.id_conflict_policy), - ), - ) - - if input.schedule_to_close_timeout is not None: - req.schedule_to_close_timeout.FromTimedelta(input.schedule_to_close_timeout) - if input.start_to_close_timeout is not None: - req.start_to_close_timeout.FromTimedelta(input.start_to_close_timeout) - if input.schedule_to_start_timeout is not None: - req.schedule_to_start_timeout.FromTimedelta(input.schedule_to_start_timeout) - if input.heartbeat_timeout is not None: - req.heartbeat_timeout.FromTimedelta(input.heartbeat_timeout) - if input.start_delay is not None: - req.start_delay.FromTimedelta(input.start_delay) - if input.retry_policy is not None: - input.retry_policy.apply_to_proto(req.retry_policy) - - # Set input payloads - if input.args: - req.input.payloads.extend(await data_converter.encode(input.args)) - - # Set search attributes - if input.search_attributes is not None: - temporalio.converter.encode_search_attributes( - input.search_attributes, req.search_attributes - ) - - # Set user metadata - metadata = await _encode_user_metadata(data_converter, input.summary, None) - if metadata is not None: - req.user_metadata.CopyFrom(metadata) - - # Set headers - if input.headers: - await self._apply_headers(input.headers, req.header.fields) - - # Set priority - req.priority.CopyFrom(input.priority._to_proto()) - - return req - - async def cancel_activity(self, input: CancelActivityInput) -> None: - """Cancel an activity.""" - await self._client.workflow_service.request_cancel_activity_execution( - temporalio.api.workflowservice.v1.RequestCancelActivityExecutionRequest( - namespace=self._client.namespace, - activity_id=input.activity_id, - run_id=input.activity_run_id or "", - identity=self._client.identity, - request_id=str(uuid.uuid4()), - reason=input.reason or "", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def terminate_activity(self, input: TerminateActivityInput) -> None: - """Terminate an activity.""" - await self._client.workflow_service.terminate_activity_execution( - temporalio.api.workflowservice.v1.TerminateActivityExecutionRequest( - namespace=self._client.namespace, - activity_id=input.activity_id, - run_id=input.activity_run_id or "", - reason=input.reason or "", - identity=self._client.identity, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def describe_activity( - self, input: DescribeActivityInput - ) -> ActivityExecutionDescription: - """Describe an activity.""" - resp = await self._client.workflow_service.describe_activity_execution( - temporalio.api.workflowservice.v1.DescribeActivityExecutionRequest( - namespace=self._client.namespace, - activity_id=input.activity_id, - run_id=input.activity_run_id or "", - long_poll_token=input.long_poll_token or b"", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - return await ActivityExecutionDescription._from_execution_info( - info=resp.info, - long_poll_token=resp.long_poll_token or None, - namespace=self._client.namespace, - data_converter=self._client.data_converter.with_context( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=input.activity_id, # Using activity_id as workflow_id for activities not started by a workflow - ) - ), - ) - - def list_activities( - self, input: ListActivitiesInput - ) -> ActivityExecutionAsyncIterator: - return ActivityExecutionAsyncIterator(self._client, input) - - async def count_activities( - self, input: CountActivitiesInput - ) -> ActivityExecutionCount: - return ActivityExecutionCount._from_raw( - await self._client.workflow_service.count_activity_executions( - temporalio.api.workflowservice.v1.CountActivityExecutionsRequest( - namespace=self._client.namespace, - query=input.query or "", - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - ) - - async def start_workflow_update( - self, input: StartWorkflowUpdateInput - ) -> WorkflowUpdateHandle[Any]: - workflow_id = input.id - req = await self._build_update_workflow_execution_request(input, workflow_id) - - # Repeatedly try to invoke UpdateWorkflowExecution until the update is durable. - resp: temporalio.api.workflowservice.v1.UpdateWorkflowExecutionResponse - while True: - try: - resp = await self._client.workflow_service.update_workflow_execution( - req, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - except RPCError as err: - if ( - err.status == RPCStatusCode.DEADLINE_EXCEEDED - or err.status == RPCStatusCode.CANCELLED - ): - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - else: - raise - except asyncio.CancelledError as err: - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - if ( - resp.stage - >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED - ): - break - - # Build the handle. If the user's wait stage is COMPLETED, make sure we - # poll for result. - handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( - client=self._client, - id=req.request.meta.update_id, - workflow_id=workflow_id, - workflow_run_id=resp.update_ref.workflow_execution.run_id, - result_type=input.ret_type, - ) - if resp.HasField("outcome"): - handle._known_outcome = resp.outcome - if input.wait_for_stage == WorkflowUpdateStage.COMPLETED: - await handle._poll_until_outcome() - return handle - - async def _build_update_workflow_execution_request( - self, - input: StartWorkflowUpdateInput | UpdateWithStartUpdateWorkflowInput, - workflow_id: str, - ) -> temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest: - data_converter = self._client.data_converter._with_contexts( - WorkflowSerializationContext( - namespace=self._client.namespace, - workflow_id=workflow_id, - ), - StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=workflow_id, - run_id=(input.run_id or None) - if isinstance(input, StartWorkflowUpdateInput) - else None, - namespace=self._client.namespace, - ), - ), - ) - run_id, first_execution_run_id = ( - ( - input.run_id, - input.first_execution_run_id, - ) - if isinstance(input, StartWorkflowUpdateInput) - else (None, None) - ) - req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( - namespace=self._client.namespace, - workflow_execution=temporalio.api.common.v1.WorkflowExecution( - workflow_id=workflow_id, - run_id=run_id or "", - ), - first_execution_run_id=first_execution_run_id or "", - request=temporalio.api.update.v1.Request( - meta=temporalio.api.update.v1.Meta( - update_id=input.update_id or str(uuid.uuid4()), - identity=self._client.identity, - ), - input=temporalio.api.update.v1.Input( - name=input.update, - ), - ), - wait_policy=temporalio.api.update.v1.WaitPolicy( - lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType( - input.wait_for_stage - ) - ), - ) - if input.args: - req.request.input.args.payloads.extend( - await data_converter.encode(input.args) - ) - if input.headers is not None: # type:ignore[reportUnnecessaryComparison] - await self._apply_headers(input.headers, req.request.input.header.fields) - return req - - async def start_update_with_start_workflow( - self, input: StartWorkflowUpdateWithStartInput - ) -> WorkflowUpdateHandle[Any]: - seen_start = False - - def on_start( - start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, - ): - nonlocal seen_start - if not seen_start: - input._on_start(start_response) - seen_start = True - - err: BaseException | None = None - - try: - return await self._start_workflow_update_with_start( - input.start_workflow_input, input.update_workflow_input, on_start - ) - except asyncio.CancelledError as _err: - err = _err - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - except RPCError as _err: - err = _err - if err.status in [ - RPCStatusCode.DEADLINE_EXCEEDED, - RPCStatusCode.CANCELLED, - ]: - raise WorkflowUpdateRPCTimeoutOrCancelledError() from err - else: - multiop_failure = ( - temporalio.api.errordetails.v1.MultiOperationExecutionFailure() - ) - if err.grpc_status.details and err.grpc_status.details[0].Unpack( - multiop_failure - ): - status = next( - ( - st - for st in multiop_failure.statuses - if ( - st.code != RPCStatusCode.OK - and not ( - st.details - and st.details[0].Is( - temporalio.api.failure.v1.MultiOperationExecutionAborted.DESCRIPTOR - ) - ) - ) - ), - None, - ) - if status and status.code in list(RPCStatusCode): - if ( - status.code == RPCStatusCode.ALREADY_EXISTS - and status.details - ): - details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() - if status.details[0].Unpack(details): - err = temporalio.exceptions.WorkflowAlreadyStartedError( - input.start_workflow_input.id, - input.start_workflow_input.workflow, - run_id=details.run_id, - ) - else: - err = RPCError( - status.message, - RPCStatusCode(status.code), - err.raw_grpc_status, - ) - raise err - finally: - if err and not seen_start: - input._on_start_error(err) - - async def _start_workflow_update_with_start( - self, - start_input: UpdateWithStartStartWorkflowInput, - update_input: UpdateWithStartUpdateWorkflowInput, - on_start: Callable[ - [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None - ], - ) -> WorkflowUpdateHandle[Any]: - start_req = ( - await self._build_update_with_start_start_workflow_execution_request( - start_input - ) - ) - update_req = await self._build_update_workflow_execution_request( - update_input, workflow_id=start_input.id - ) - multiop_req = temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest( - namespace=self._client.namespace, - operations=[ - temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( - start_workflow=start_req - ), - temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( - update_workflow=update_req - ), - ], - ) - - # Repeatedly try to invoke ExecuteMultiOperation until the update is durable - while True: - multiop_response = ( - await self._client.workflow_service.execute_multi_operation(multiop_req) - ) - start_response = multiop_response.responses[0].start_workflow - update_response = multiop_response.responses[1].update_workflow - on_start(start_response) - known_outcome = ( - update_response.outcome if update_response.HasField("outcome") else None - ) - if ( - update_response.stage - >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED - ): - break - - handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( - client=self._client, - id=update_req.request.meta.update_id, - workflow_id=start_input.id, - workflow_run_id=start_response.run_id, - known_outcome=known_outcome, - result_type=update_input.ret_type, - ) - if update_input.wait_for_stage == WorkflowUpdateStage.COMPLETED: - await handle._poll_until_outcome() - - return handle - - ### Async activity calls - - def _get_async_activity_store_context( - self, id_or_token: AsyncActivityIDReference | bytes - ) -> StorageDriverStoreContext: - if isinstance(id_or_token, AsyncActivityIDReference): - if id_or_token.workflow_id: - return StorageDriverStoreContext( - target=StorageDriverWorkflowInfo( - id=id_or_token.workflow_id or None, - run_id=id_or_token.run_id or None, - namespace=self._client.namespace, - ), - ) - return StorageDriverStoreContext( - target=StorageDriverActivityInfo( - id=id_or_token.activity_id, - run_id=id_or_token.run_id or None, - namespace=self._client.namespace, - ), - ) - else: - return StorageDriverStoreContext(target=None) - - async def heartbeat_async_activity( - self, input: HeartbeatAsyncActivityInput - ) -> None: - data_converter = ( - input.data_converter_override or self._client.data_converter - )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) - details = ( - None - if not input.details - else await data_converter.encode_wrapper(input.details) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - resp_by_id = await self._client.workflow_service.record_activity_task_heartbeat_by_id( - temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest( - workflow_id=input.id_or_token.workflow_id or "", - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - if ( - resp_by_id.cancel_requested - or resp_by_id.activity_paused - or resp_by_id.activity_reset - ): - raise AsyncActivityCancelledError( - details=ActivityCancellationDetails( - cancel_requested=resp_by_id.cancel_requested, - paused=resp_by_id.activity_paused, - reset=resp_by_id.activity_reset, - ) - ) - - else: - resp = await self._client.workflow_service.record_activity_task_heartbeat( - temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - if resp.cancel_requested or resp.activity_paused: - raise AsyncActivityCancelledError( - details=ActivityCancellationDetails( - cancel_requested=resp.cancel_requested, - paused=resp.activity_paused, - reset=resp.activity_reset, - ) - ) - - async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: - data_converter = ( - input.data_converter_override or self._client.data_converter - )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) - result = ( - None - if input.result is temporalio.common._arg_unset - else await data_converter.encode_wrapper([input.result]) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - await self._client.workflow_service.respond_activity_task_completed_by_id( - temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest( - workflow_id=input.id_or_token.workflow_id or "", - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - result=result, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - await self._client.workflow_service.respond_activity_task_completed( - temporalio.api.workflowservice.v1.RespondActivityTaskCompletedRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - result=result, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: - data_converter = ( - input.data_converter_override or self._client.data_converter - )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) - - failure = temporalio.api.failure.v1.Failure() - await data_converter.encode_failure(input.error, failure) - last_heartbeat_details = ( - await data_converter.encode_wrapper(input.last_heartbeat_details) - if input.last_heartbeat_details - else None - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - await self._client.workflow_service.respond_activity_task_failed_by_id( - temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest( - workflow_id=input.id_or_token.workflow_id or "", - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - failure=failure, - last_heartbeat_details=last_heartbeat_details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - await self._client.workflow_service.respond_activity_task_failed( - temporalio.api.workflowservice.v1.RespondActivityTaskFailedRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - failure=failure, - last_heartbeat_details=last_heartbeat_details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def report_cancellation_async_activity( - self, input: ReportCancellationAsyncActivityInput - ) -> None: - data_converter = ( - input.data_converter_override or self._client.data_converter - )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) - details = ( - None - if not input.details - else await data_converter.encode_wrapper(input.details) - ) - if isinstance(input.id_or_token, AsyncActivityIDReference): - await self._client.workflow_service.respond_activity_task_canceled_by_id( - temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest( - workflow_id=input.id_or_token.workflow_id or "", - run_id=input.id_or_token.run_id or "", - activity_id=input.id_or_token.activity_id, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - else: - await self._client.workflow_service.respond_activity_task_canceled( - temporalio.api.workflowservice.v1.RespondActivityTaskCanceledRequest( - task_token=input.id_or_token, - namespace=self._client.namespace, - identity=self._client.identity, - details=details, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - ### 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: temporalio.api.schedule.v1.SchedulePatch | None = None - if input.trigger_immediately or input.backfill: - initial_patch = temporalio.api.schedule.v1.SchedulePatch( - trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( - overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - input.schedule.policy.overlap - ), - ) - if input.trigger_immediately - else None, - backfill_request=[b._to_proto() for b in input.backfill] - if input.backfill - else None, - ) - try: - request = temporalio.api.workflowservice.v1.CreateScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - schedule=await input.schedule._to_proto(self._client), - initial_patch=initial_patch, - identity=self._client.identity, - request_id=str(uuid.uuid4()), - memo=await self._client.data_converter._encode_memo(input.memo) - if input.memo - else None, - ) - if input.search_attributes: - temporalio.converter.encode_search_attributes( - input.search_attributes, request.search_attributes - ) - await self._client.workflow_service.create_schedule( - request, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - except RPCError as err: - already_started = ( - err.status == RPCStatusCode.ALREADY_EXISTS - and err.grpc_status.details - and err.grpc_status.details[0].Is( - temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure.DESCRIPTOR - ) - ) - if already_started: - raise ScheduleAlreadyRunningError() - raise - return ScheduleHandle(self._client, input.id) - - def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: - return ScheduleAsyncIterator(self._client, input) - - async def backfill_schedule(self, input: BackfillScheduleInput) -> None: - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - backfill_request=[b._to_proto() for b in input.backfills], - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def delete_schedule(self, input: DeleteScheduleInput) -> None: - await self._client.workflow_service.delete_schedule( - temporalio.api.workflowservice.v1.DeleteScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - identity=self._client.identity, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def describe_schedule( - self, input: DescribeScheduleInput - ) -> ScheduleDescription: - return ScheduleDescription._from_proto( - input.id, - await self._client.workflow_service.describe_schedule( - temporalio.api.workflowservice.v1.DescribeScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ), - self._client.data_converter, - ) - - async def pause_schedule(self, input: PauseScheduleInput) -> None: - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - pause=input.note or "Paused via Python SDK", - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def trigger_schedule(self, input: TriggerScheduleInput) -> None: - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED - if input.overlap: - overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( - input.overlap - ) - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( - overlap_policy=overlap_policy, - ), - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: - await self._client.workflow_service.patch_schedule( - temporalio.api.workflowservice.v1.PatchScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - patch=temporalio.api.schedule.v1.SchedulePatch( - unpause=input.note or "Unpaused via Python SDK", - ), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def update_schedule(self, input: UpdateScheduleInput) -> None: - # TODO(cretz): This is supposed to be a retry-conflict loop, but we do - # not yet have a way to know update failure is due to conflict token - # mismatch - update = input.updater( - ScheduleUpdateInput( - description=ScheduleDescription._from_proto( - input.id, - await self._client.workflow_service.describe_schedule( - temporalio.api.workflowservice.v1.DescribeScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - ), - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ), - self._client.data_converter, - ) - ) - ) - if inspect.iscoroutine(update): - update = await update - if not update: - return - assert isinstance(update, ScheduleUpdate) - request = temporalio.api.workflowservice.v1.UpdateScheduleRequest( - namespace=self._client.namespace, - schedule_id=input.id, - schedule=await update.schedule._to_proto(self._client), - identity=self._client.identity, - request_id=str(uuid.uuid4()), - ) - if update.search_attributes is not None: - request.search_attributes.indexed_fields.clear() # Ensure that we at least create an empty map - temporalio.converter.encode_search_attributes( - update.search_attributes, request.search_attributes - ) - await self._client.workflow_service.update_schedule( - request, - retry=True, - metadata=input.rpc_metadata, - timeout=input.rpc_timeout, - ) - - async def update_worker_build_id_compatibility( - self, input: UpdateWorkerBuildIdCompatibilityInput - ) -> None: - req = input.operation._as_partial_proto() - req.namespace = self._client.namespace - req.task_queue = input.task_queue - await self._client.workflow_service.update_worker_build_id_compatibility( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - - async def get_worker_build_id_compatibility( - self, input: GetWorkerBuildIdCompatibilityInput - ) -> WorkerBuildIdVersionSets: - req = temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest( - namespace=self._client.namespace, - task_queue=input.task_queue, - max_sets=input.max_sets or 0, - ) - resp = await self._client.workflow_service.get_worker_build_id_compatibility( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - return WorkerBuildIdVersionSets._from_proto(resp) - - async def get_worker_task_reachability( - self, input: GetWorkerTaskReachabilityInput - ) -> WorkerTaskReachability: - req = temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityRequest( - namespace=self._client.namespace, - build_ids=input.build_ids, - task_queues=input.task_queues, - reachability=input.reachability._to_proto() - if input.reachability - else temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED, - ) - resp = await self._client.workflow_service.get_worker_task_reachability( - req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout - ) - return WorkerTaskReachability._from_proto(resp) - - async def _apply_headers( - self, - source: Mapping[str, temporalio.api.common.v1.Payload] | None, - dest: MessageMap[str, temporalio.api.common.v1.Payload], - ) -> None: - await _apply_headers( - source, - dest, - self._client.config(active_config=True)["header_codec_behavior"] - == HeaderCodecBehavior.CODEC, - self._client.data_converter, - ) - - -async def _apply_headers( - source: Mapping[str, temporalio.api.common.v1.Payload] | None, - dest: MessageMap[str, temporalio.api.common.v1.Payload], - encode_headers: bool, - data_converter: DataConverter, -) -> None: - if source is None: - return - if encode_headers: - for payload in source.values(): - payload.CopyFrom(await data_converter._transform_outbound_payload(payload)) - temporalio.common._apply_headers(source, dest) - - -def _history_from_json( - history: str | dict[str, Any], -) -> temporalio.api.history.v1.History: - if isinstance(history, str): - history = json.loads(history) - else: - # Copy the dict so we can mutate it - history = copy.deepcopy(history) - if not isinstance(history, dict): - raise ValueError("JSON history not a dictionary") - events = history.get("events") - if not isinstance(events, Iterable): - raise ValueError("History does not have iterable 'events'") - for event in events: - if not isinstance(event, dict): - raise ValueError("Event not a dictionary") - _fix_history_enum( - "CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", - event, - "requestCancelExternalWorkflowExecutionFailedEventAttributes", - "cause", - ) - _fix_history_enum("CONTINUE_AS_NEW_INITIATOR", event, "*", "initiator") - _fix_history_enum("EVENT_TYPE", event, "eventType") - _fix_history_enum( - "PARENT_CLOSE_POLICY", - event, - "startChildWorkflowExecutionInitiatedEventAttributes", - "parentClosePolicy", - ) - _fix_history_enum("RETRY_STATE", event, "*", "retryState") - _fix_history_enum( - "SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", - event, - "signalExternalWorkflowExecutionFailedEventAttributes", - "cause", - ) - _fix_history_enum( - "START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE", - event, - "startChildWorkflowExecutionFailedEventAttributes", - "cause", - ) - _fix_history_enum("TASK_QUEUE_KIND", event, "*", "taskQueue", "kind") - _fix_history_enum( - "TIMEOUT_TYPE", - event, - "workflowTaskTimedOutEventAttributes", - "timeoutType", - ) - _fix_history_enum( - "WORKFLOW_ID_REUSE_POLICY", - event, - "startChildWorkflowExecutionInitiatedEventAttributes", - "workflowIdReusePolicy", - ) - _fix_history_enum( - "WORKFLOW_TASK_FAILED_CAUSE", - event, - "workflowTaskFailedEventAttributes", - "cause", - ) - _fix_history_failure(event, "*", "failure") - _fix_history_failure(event, "activityTaskStartedEventAttributes", "lastFailure") - _fix_history_failure( - event, "workflowExecutionStartedEventAttributes", "continuedFailure" - ) - return google.protobuf.json_format.ParseDict( - history, temporalio.api.history.v1.History(), ignore_unknown_fields=True - ) - - -_pascal_case_match = re.compile("([A-Z]+)") - - -def _fix_history_failure(parent: dict[str, Any], *attrs: str) -> None: - _fix_history_enum( - "TIMEOUT_TYPE", parent, *attrs, "timeoutFailureInfo", "timeoutType" - ) - _fix_history_enum("RETRY_STATE", parent, *attrs, "*", "retryState") - # Recurse into causes. First collect all failure parents. - parents = [parent] - for attr in attrs: - new_parents = [] - for parent in parents: - if attr == "*": - for v in parent.values(): - if isinstance(v, dict): - new_parents.append(v) - else: - child = parent.get(attr) - if isinstance(child, dict): - new_parents.append(child) - if not new_parents: - return - parents = new_parents - # Fix each - for parent in parents: - _fix_history_failure(parent, "cause") - - -def _fix_history_enum(prefix: str, parent: dict[str, Any], *attrs: str) -> None: - # If the attr is "*", we need to handle all dict children - if attrs[0] == "*": - for child in parent.values(): - if isinstance(child, dict): - _fix_history_enum(prefix, child, *attrs[1:]) - else: - child = parent.get(attrs[0]) - if isinstance(child, str) and len(attrs) == 1: - # We only fix it if it doesn't already have the prefix - if not parent[attrs[0]].startswith(prefix): - parent[attrs[0]] = ( - prefix + _pascal_case_match.sub(r"_\1", child).upper() - ) - elif isinstance(child, dict) and len(attrs) > 1: - _fix_history_enum(prefix, child, *attrs[1:]) - elif isinstance(child, list) and len(attrs) > 1: - for child_item in child: - if isinstance(child_item, dict): - _fix_history_enum(prefix, child_item, *attrs[1:]) - - -@dataclass(frozen=True) -class WorkerBuildIdVersionSets: - """Represents the sets of compatible Build ID versions associated with some Task Queue, as - fetched by :py:meth:`Client.get_worker_build_id_compatibility`. - """ - - version_sets: Sequence[BuildIdVersionSet] - """All version sets that were fetched for this task queue.""" - - def default_set(self) -> BuildIdVersionSet: - """Returns the default version set for this task queue.""" - return self.version_sets[-1] - - def default_build_id(self) -> str: - """Returns the default Build ID for this task queue.""" - return self.default_set().default() - - @staticmethod - def _from_proto( - resp: temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse, - ) -> WorkerBuildIdVersionSets: - return WorkerBuildIdVersionSets( - version_sets=[ - BuildIdVersionSet(mvs.build_ids) for mvs in resp.major_version_sets - ] - ) - - -@dataclass(frozen=True) -class BuildIdVersionSet: - """A set of Build IDs which are compatible with each other.""" - - build_ids: Sequence[str] - """All Build IDs contained in the set.""" - - def default(self) -> str: - """Returns the default Build ID for this set.""" - return self.build_ids[-1] - - -class BuildIdOp(ABC): - """Base class for Build ID operations as used by - :py:meth:`Client.update_worker_build_id_compatibility`. - """ - - @abstractmethod - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - """Returns a partial request with the operation populated. Caller must populate - non-operation fields. This is done b/c there's no good way to assign a non-primitive message - as the operation after initializing the request. - """ - ... - - -@dataclass(frozen=True) -class BuildIdOpAddNewDefault(BuildIdOp): - """Adds a new Build Id into a new set, which will be used as the default set for - the queue. This means all new workflows will start on this Build Id. - """ - - build_id: str - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return ( - temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - add_new_build_id_in_new_default_set=self.build_id - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpAddNewCompatible(BuildIdOp): - """Adds a new Build Id into an existing compatible set. The newly added ID becomes - the default for that compatible set, and thus new workflow tasks for workflows which have been - executing on workers in that set will now start on this new Build Id. - """ - - build_id: str - """The Build Id to add to the compatible set.""" - - existing_compatible_build_id: str - """A Build Id which must already be defined on the task queue, and is used to find the - compatible set to add the new id to. - """ - - promote_set: bool = False - """If set to true, the targeted set will also be promoted to become the overall default set for - the queue.""" - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - add_new_compatible_build_id=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion( - new_build_id=self.build_id, - existing_compatible_build_id=self.existing_compatible_build_id, - make_set_default=self.promote_set, - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpPromoteSetByBuildId(BuildIdOp): - """Promotes a set of compatible Build Ids to become the current default set for the task queue. - Any Build Id in the set may be used to target it. - """ - - build_id: str - """A Build Id which must already be defined on the task queue, and is used to find the - compatible set to promote.""" - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return ( - temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - promote_set_by_build_id=self.build_id - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpPromoteBuildIdWithinSet(BuildIdOp): - """Promotes a Build Id within an existing set to become the default ID for that set.""" - - build_id: str - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return ( - temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - promote_build_id_within_set=self.build_id - ) - ) - - -@dataclass(frozen=True) -class BuildIdOpMergeSets(BuildIdOp): - """Merges two sets into one set, thus declaring all the Build Ids in both as compatible with one - another. The default of the primary set is maintained as the merged set's overall default. - """ - - primary_build_id: str - """A Build Id which and is used to find the primary set to be merged.""" - - secondary_build_id: str - """A Build Id which and is used to find the secondary set to be merged.""" - - def _as_partial_proto( - self, - ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: - return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( - merge_sets=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSets( - primary_set_build_id=self.primary_build_id, - secondary_set_build_id=self.secondary_build_id, - ) - ) - - -@dataclass(frozen=True) -class WorkerTaskReachability: - """Contains information about the reachability of some Build IDs""" - - build_id_reachability: Mapping[str, BuildIdReachability] - """Maps Build IDs to information about their reachability""" - - @staticmethod - def _from_proto( - resp: temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityResponse, - ) -> WorkerTaskReachability: - mapping = dict() - for bid_reach in resp.build_id_reachability: - tq_mapping = dict() - unretrieved = set() - for tq_reach in bid_reach.task_queue_reachability: - if tq_reach.reachability == [ - temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED - ]: - unretrieved.add(tq_reach.task_queue) - continue - tq_mapping[tq_reach.task_queue] = [ - TaskReachabilityType._from_proto(r) for r in tq_reach.reachability - ] - - mapping[bid_reach.build_id] = BuildIdReachability( - task_queue_reachability=tq_mapping, - unretrieved_task_queues=frozenset(unretrieved), - ) - - return WorkerTaskReachability(build_id_reachability=mapping) - - -@dataclass(frozen=True) -class BuildIdReachability: - """Contains information about the reachability of a specific Build ID""" - - task_queue_reachability: Mapping[str, Sequence[TaskReachabilityType]] - """Maps Task Queue names to the reachability status of the Build ID on that queue. If the value - is an empty list, the Build ID is not reachable on that queue. - """ - - unretrieved_task_queues: frozenset[str] - """If any Task Queues could not be retrieved because the server limits the number that can be - queried at once, they will be listed here. - """ - - -class TaskReachabilityType(Enum): - """Enumerates how a task might reach certain kinds of workflows""" - - NEW_WORKFLOWS = 1 - EXISTING_WORKFLOWS = 2 - OPEN_WORKFLOWS = 3 - CLOSED_WORKFLOWS = 4 - - @staticmethod - def _from_proto( - reachability: temporalio.api.enums.v1.TaskReachability.ValueType, - ) -> TaskReachabilityType: - if ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS - ): - return TaskReachabilityType.NEW_WORKFLOWS - elif ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS - ): - return TaskReachabilityType.EXISTING_WORKFLOWS - elif ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS - ): - return TaskReachabilityType.OPEN_WORKFLOWS - elif ( - reachability - == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS - ): - return TaskReachabilityType.CLOSED_WORKFLOWS - else: - raise ValueError(f"Cannot convert reachability type: {reachability}") - - def _to_proto(self) -> temporalio.api.enums.v1.TaskReachability.ValueType: - if self == TaskReachabilityType.NEW_WORKFLOWS: - return ( - temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS - ) - elif self == TaskReachabilityType.EXISTING_WORKFLOWS: - return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS - elif self == TaskReachabilityType.OPEN_WORKFLOWS: - return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS - elif self == TaskReachabilityType.CLOSED_WORKFLOWS: - return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS - else: - return ( - temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED - ) - - -class CloudOperationsClient: - """Client for accessing Temporal Cloud Operations API. - - .. warning:: - This client and the API are experimental - - Most users will use :py:meth:`connect` to create a client. The - :py:attr:`cloud_service` property provides access to a raw gRPC cloud - service client. - - Clients are not thread-safe and should only be used in the event loop they - are first connected in. If a client needs to be used from another thread - than where it was created, make sure the event loop where it was created is - captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the - client call and that event loop. - - Clients do not work across forks since runtimes do not work across forks. - """ - - @staticmethod - async def connect( - *, - api_key: str | None = None, - version: str | None = None, - target_host: str = "saas-api.tmprl.cloud:443", - tls: bool | TLSConfig = True, - retry_config: RetryConfig | None = None, - keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default, - rpc_metadata: Mapping[str, str | bytes] = {}, - identity: str | None = None, - lazy: bool = False, - runtime: temporalio.runtime.Runtime | None = None, - http_connect_proxy_config: HttpConnectProxyConfig | None = None, - dns_load_balancing_config: DnsLoadBalancingConfig | None = None, - ) -> CloudOperationsClient: - """Connect to a Temporal Cloud Operations API. - - .. warning:: - This client and the API are experimental - - Args: - api_key: API key for Temporal. This becomes the "Authorization" - HTTP header with "Bearer " prepended. This is only set if RPC - metadata doesn't already have an "authorization" key. This is - essentially required for access to the cloud API. - version: Version header for safer mutations. May or may not be - required depending on cloud settings. - target_host: ``host:port`` for the Temporal server. The default is - to the common cloud endpoint. - tls: If true, the default, use system default TLS configuration. If - false, the default, do not use TLS. If TLS configuration - present, that TLS configuration will be used. The default is - usually required to access the API. - retry_config: Retry configuration for direct service calls (when - opted in) or all high-level calls made by this client (which all - opt-in to retries by default). If unset, a default retry - configuration is used. - keep_alive_config: Keep-alive configuration for the client - connection. Default is to check every 30s and kill the - connection if a response doesn't come back in 15s. Can be set to - ``None`` to disable. - rpc_metadata: Headers to use for all calls to the server. Keys here - can be overriden by per-call RPC metadata keys. - identity: Identity for this client. If unset, a default is created - based on the version of the SDK. - lazy: If true, the client will not connect until the first call is - attempted or a worker is created with it. Lazy clients cannot be - used for workers. - runtime: The runtime for this client, or the default if unset. - http_connect_proxy_config: Configuration for HTTP CONNECT proxy. - dns_load_balancing_config: DNS load balancing configuration for the - client connection. Default is disabled. Silently disabled when - ``http_connect_proxy_config`` is set, since the two are mutually - exclusive. - """ - # Add version if given - if version: - rpc_metadata = dict(rpc_metadata) - rpc_metadata["temporal-cloud-api-version"] = version - connect_config = temporalio.service.ConnectConfig( - target_host=target_host, - api_key=api_key, - tls=tls, - retry_config=retry_config, - keep_alive_config=keep_alive_config, - rpc_metadata=rpc_metadata, - identity=identity or "", - lazy=lazy, - runtime=runtime, - http_connect_proxy_config=http_connect_proxy_config, - dns_load_balancing_config=dns_load_balancing_config, - ) - return CloudOperationsClient( - await temporalio.service.ServiceClient.connect(connect_config) - ) - - def __init__( - self, - service_client: temporalio.service.ServiceClient, - ): - """Create a Temporal Cloud Operations client from a service client. - - .. warning:: - This client and the API are experimental - - Args: - service_client: Existing service client to use. - """ - self._service_client = service_client - - @property - def service_client(self) -> temporalio.service.ServiceClient: - """Raw gRPC service client.""" - return self._service_client - - @property - def cloud_service(self) -> temporalio.service.CloudService: - """Raw gRPC cloud service client.""" - return self._service_client.cloud_service - - @property - def identity(self) -> str: - """Identity used in calls by this client.""" - return self._service_client.config.identity - - @property - def rpc_metadata(self) -> Mapping[str, str | bytes]: - """Headers for every call made by this client. - - Do not use mutate this mapping. Rather, set this property with an - entirely new mapping to change the headers. This may include the - ``temporal-cloud-api-version`` header if set. - """ - return self.service_client.config.rpc_metadata - - @rpc_metadata.setter - def rpc_metadata(self, value: Mapping[str, str | bytes]) -> None: - """Update the headers for this client. - - Do not mutate this mapping after set. Rather, set an entirely new - mapping if changes are needed. Currently this must be set with the - ``temporal-cloud-api-version`` header if it is needed. - """ - # Update config and perform update - self.service_client.config.rpc_metadata = value - self.service_client.update_rpc_metadata(value) - - @property - def api_key(self) -> str | None: - """API key for every call made by this client.""" - return self.service_client.config.api_key - - @api_key.setter - def api_key(self, value: str | None) -> None: - """Update the API key for this client. - - This is only set if RPCmetadata doesn't already have an "authorization" - key. - """ - # Update config and perform update - self.service_client.config.api_key = value - self.service_client.update_api_key(value) - - -# Intended to become a union of callback types -Callback = temporalio.nexus.NexusCallback - - -async def _encode_user_metadata( - converter: temporalio.converter.DataConverter, - summary: str | temporalio.api.common.v1.Payload | None, - details: str | temporalio.api.common.v1.Payload | None, -) -> temporalio.api.sdk.v1.UserMetadata | None: - if summary is None and details is None: - return None - enc_summary = None - enc_details = None - if summary is not None: - if isinstance(summary, str): - enc_summary = (await converter.encode([summary]))[0] - else: - enc_summary = summary - if details is not None: - if isinstance(details, str): - enc_details = (await converter.encode([details]))[0] - else: - enc_details = details - return temporalio.api.sdk.v1.UserMetadata(summary=enc_summary, details=enc_details) - - -async def _decode_user_metadata( - converter: temporalio.converter.DataConverter, - metadata: temporalio.api.sdk.v1.UserMetadata | None, -) -> tuple[str | None, str | None]: - """Returns (summary, details)""" - if metadata is None: - return None, None - return ( - None - if not metadata.HasField("summary") - else (await converter.decode([metadata.summary]))[0], - None - if not metadata.HasField("details") - else (await converter.decode([metadata.details]))[0], - ) - - -class Plugin(abc.ABC): - """Base class for client plugins that can intercept and modify client behavior. - - Plugins allow customization of client creation and service connection processes - through a chain of responsibility pattern. Each plugin can modify the client - configuration or intercept service client connections. - - If the plugin is also a temporalio.worker.Plugin, it will additionally be propagated as a worker plugin. - You should likley not also provide it to the worker as that will result in the plugin being applied twice. - """ - - def name(self) -> str: - """Get the name of this plugin. Can be overridden if desired to provide a more appropriate name. - - Returns: - The fully qualified name of the plugin class (module.classname). - """ - return type(self).__module__ + "." + type(self).__qualname__ - - @abstractmethod - def configure_client(self, config: ClientConfig) -> ClientConfig: - """Hook called when creating a client to allow modification of configuration. - - This method is called during client creation and allows plugins to modify - the client configuration before the client is fully initialized. Plugins - can add interceptors, modify connection parameters, or change other settings. - - Args: - config: The client configuration dictionary to potentially modify. - - Returns: - The modified client configuration. - """ - - @abstractmethod - async def connect_service_client( - self, - config: ConnectConfig, - next: Callable[[ConnectConfig], Awaitable[ServiceClient]], - ) -> ServiceClient: - """Hook called when connecting to the Temporal service. - - This method is called during service client connection and allows plugins - to intercept or modify the connection process. Plugins can modify connection - parameters, add authentication, or provide custom connection logic. - - Args: - config: The service connection configuration. - - Returns: - The connected service client. - """ diff --git a/temporalio/client/__init__.py b/temporalio/client/__init__.py new file mode 100644 index 000000000..c403b8bcc --- /dev/null +++ b/temporalio/client/__init__.py @@ -0,0 +1,346 @@ +"""Client for accessing Temporal.""" + +from __future__ import annotations + +from temporalio.activity import ActivityCancellationDetails +from temporalio.converter import ( + ActivitySerializationContext, + DataConverter, + SerializationContext, + StorageDriverActivityInfo, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + WithSerializationContext, + WorkflowSerializationContext, +) +from temporalio.service import ( + ConnectConfig, + DnsLoadBalancingConfig, + HttpConnectProxyConfig, + KeepAliveConfig, + RetryConfig, + RPCError, + RPCStatusCode, + ServiceClient, + TLSConfig, +) + +from ..common import HeaderCodecBehavior +from ..types import ( + AnyType, + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableSyncNoParam, + CallableSyncSingleParam, + LocalReturnType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncOrAsyncNoParam, + MethodSyncOrAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._activity import ( + ActivityExecution, + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityExecutionCountAggregationGroup, + ActivityExecutionDescription, + ActivityExecutionStatus, + ActivityHandle, + AsyncActivityHandle, + AsyncActivityIDReference, + PendingActivityState, +) +from ._callback import ( + Callback, +) +from ._client import ( + Client, + ClientConfig, + ClientConnectConfig, +) +from ._cloud import ( + CloudOperationsClient, +) +from ._exceptions import ( + ActivityFailureError, + AsyncActivityCancelledError, + RPCTimeoutOrCancelledError, + ScheduleAlreadyRunningError, + WorkflowContinuedAsNewError, + WorkflowFailureError, + WorkflowQueryFailedError, + WorkflowQueryRejectedError, + WorkflowUpdateFailedError, + WorkflowUpdateRPCTimeoutOrCancelledError, +) +from ._helpers import ( + _apply_headers, + _decode_user_metadata, + _encode_user_metadata, + _fix_history_enum, + _fix_history_failure, + _history_from_json, + _pascal_case_match, +) +from ._impl import _ClientImpl +from ._interceptor import ( + BackfillScheduleInput, + CancelActivityInput, + CancelWorkflowInput, + CompleteAsyncActivityInput, + CountActivitiesInput, + CountWorkflowsInput, + CreateScheduleInput, + DeleteScheduleInput, + DescribeActivityInput, + DescribeScheduleInput, + DescribeWorkflowInput, + FailAsyncActivityInput, + FetchWorkflowHistoryEventsInput, + GetWorkerBuildIdCompatibilityInput, + GetWorkerTaskReachabilityInput, + HeartbeatAsyncActivityInput, + Interceptor, + ListActivitiesInput, + ListSchedulesInput, + ListWorkflowsInput, + OutboundInterceptor, + PauseScheduleInput, + QueryWorkflowInput, + ReportCancellationAsyncActivityInput, + SignalWorkflowInput, + StartActivityInput, + StartWorkflowInput, + StartWorkflowUpdateInput, + StartWorkflowUpdateWithStartInput, + TerminateActivityInput, + TerminateWorkflowInput, + TriggerScheduleInput, + UnpauseScheduleInput, + UpdateScheduleInput, + UpdateWithStartStartWorkflowInput, + UpdateWithStartUpdateWorkflowInput, + UpdateWorkerBuildIdCompatibilityInput, +) +from ._plugin import ( + Plugin, +) +from ._schedule import ( + Schedule, + ScheduleAction, + ScheduleActionExecution, + ScheduleActionExecutionStartWorkflow, + ScheduleActionResult, + ScheduleActionStartWorkflow, + ScheduleAsyncIterator, + ScheduleBackfill, + ScheduleCalendarSpec, + ScheduleDescription, + ScheduleHandle, + ScheduleInfo, + ScheduleIntervalSpec, + ScheduleListAction, + ScheduleListActionStartWorkflow, + ScheduleListDescription, + ScheduleListInfo, + ScheduleListSchedule, + ScheduleListState, + ScheduleOverlapPolicy, + SchedulePolicy, + ScheduleRange, + ScheduleSpec, + ScheduleState, + ScheduleUpdate, + ScheduleUpdateInput, +) +from ._worker_versioning import ( + BuildIdOp, + BuildIdOpAddNewCompatible, + BuildIdOpAddNewDefault, + BuildIdOpMergeSets, + BuildIdOpPromoteBuildIdWithinSet, + BuildIdOpPromoteSetByBuildId, + BuildIdReachability, + BuildIdVersionSet, + TaskReachabilityType, + WorkerBuildIdVersionSets, + WorkerTaskReachability, +) +from ._workflow import ( + WithStartWorkflowOperation, + WorkflowExecution, + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowExecutionCountAggregationGroup, + WorkflowExecutionDescription, + WorkflowExecutionStatus, + WorkflowHandle, + WorkflowHistory, + WorkflowHistoryEventAsyncIterator, + WorkflowHistoryEventFilterType, + WorkflowUpdateHandle, + WorkflowUpdateStage, +) + +__all__ = [ + "Client", + "ClientConnectConfig", + "ClientConfig", + "WorkflowHistoryEventFilterType", + "WorkflowHandle", + "WithStartWorkflowOperation", + "WorkflowExecution", + "WorkflowExecutionDescription", + "WorkflowExecutionStatus", + "WorkflowExecutionCount", + "WorkflowExecutionCountAggregationGroup", + "WorkflowExecutionAsyncIterator", + "WorkflowHistory", + "WorkflowHistoryEventAsyncIterator", + "WorkflowUpdateHandle", + "WorkflowUpdateStage", + "ActivityExecutionAsyncIterator", + "ActivityExecution", + "ActivityExecutionDescription", + "ActivityExecutionStatus", + "PendingActivityState", + "ActivityExecutionCount", + "ActivityExecutionCountAggregationGroup", + "AsyncActivityIDReference", + "AsyncActivityHandle", + "ActivityHandle", + "ScheduleHandle", + "ScheduleSpec", + "ScheduleRange", + "ScheduleCalendarSpec", + "ScheduleIntervalSpec", + "ScheduleAction", + "ScheduleActionStartWorkflow", + "ScheduleOverlapPolicy", + "ScheduleBackfill", + "SchedulePolicy", + "ScheduleState", + "Schedule", + "ScheduleDescription", + "ScheduleInfo", + "ScheduleActionExecution", + "ScheduleActionExecutionStartWorkflow", + "ScheduleActionResult", + "ScheduleUpdateInput", + "ScheduleUpdate", + "ScheduleListDescription", + "ScheduleListSchedule", + "ScheduleListAction", + "ScheduleListActionStartWorkflow", + "ScheduleListInfo", + "ScheduleListState", + "ScheduleAsyncIterator", + "WorkflowFailureError", + "WorkflowContinuedAsNewError", + "WorkflowQueryRejectedError", + "WorkflowQueryFailedError", + "WorkflowUpdateFailedError", + "RPCTimeoutOrCancelledError", + "WorkflowUpdateRPCTimeoutOrCancelledError", + "ActivityFailureError", + "AsyncActivityCancelledError", + "ScheduleAlreadyRunningError", + "StartWorkflowInput", + "CancelWorkflowInput", + "DescribeWorkflowInput", + "FetchWorkflowHistoryEventsInput", + "ListWorkflowsInput", + "CountWorkflowsInput", + "QueryWorkflowInput", + "SignalWorkflowInput", + "TerminateWorkflowInput", + "StartActivityInput", + "CancelActivityInput", + "TerminateActivityInput", + "DescribeActivityInput", + "ListActivitiesInput", + "CountActivitiesInput", + "StartWorkflowUpdateInput", + "UpdateWithStartUpdateWorkflowInput", + "UpdateWithStartStartWorkflowInput", + "StartWorkflowUpdateWithStartInput", + "HeartbeatAsyncActivityInput", + "CompleteAsyncActivityInput", + "FailAsyncActivityInput", + "ReportCancellationAsyncActivityInput", + "CreateScheduleInput", + "ListSchedulesInput", + "BackfillScheduleInput", + "DeleteScheduleInput", + "DescribeScheduleInput", + "PauseScheduleInput", + "TriggerScheduleInput", + "UnpauseScheduleInput", + "UpdateScheduleInput", + "UpdateWorkerBuildIdCompatibilityInput", + "GetWorkerBuildIdCompatibilityInput", + "GetWorkerTaskReachabilityInput", + "Interceptor", + "OutboundInterceptor", + "WorkerBuildIdVersionSets", + "BuildIdVersionSet", + "BuildIdOp", + "BuildIdOpAddNewDefault", + "BuildIdOpAddNewCompatible", + "BuildIdOpPromoteSetByBuildId", + "BuildIdOpPromoteBuildIdWithinSet", + "BuildIdOpMergeSets", + "WorkerTaskReachability", + "BuildIdReachability", + "TaskReachabilityType", + "CloudOperationsClient", + "Plugin", + "Callback", + "_ClientImpl", + "_apply_headers", + "_decode_user_metadata", + "_encode_user_metadata", + "_fix_history_enum", + "_fix_history_failure", + "_history_from_json", + "_pascal_case_match", + # Re-export Temporal-owned names that old temporalio/client.py imported at + # module scope so explicit imports from temporalio.client keep working. + "ActivityCancellationDetails", + "ActivitySerializationContext", + "DataConverter", + "SerializationContext", + "StorageDriverActivityInfo", + "StorageDriverStoreContext", + "StorageDriverWorkflowInfo", + "WithSerializationContext", + "WorkflowSerializationContext", + "ConnectConfig", + "DnsLoadBalancingConfig", + "HttpConnectProxyConfig", + "KeepAliveConfig", + "RetryConfig", + "RPCError", + "RPCStatusCode", + "ServiceClient", + "TLSConfig", + "HeaderCodecBehavior", + "AnyType", + "CallableAsyncNoParam", + "CallableAsyncSingleParam", + "CallableSyncNoParam", + "CallableSyncSingleParam", + "LocalReturnType", + "MethodAsyncNoParam", + "MethodAsyncSingleParam", + "MethodSyncOrAsyncNoParam", + "MethodSyncOrAsyncSingleParam", + "MultiParamSpec", + "ParamType", + "ReturnType", + "SelfType", +] diff --git a/temporalio/client/_activity.py b/temporalio/client/_activity.py new file mode 100644 index 000000000..99c9ede31 --- /dev/null +++ b/temporalio/client/_activity.py @@ -0,0 +1,913 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import asyncio +import functools +import warnings +from collections.abc import ( + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import IntEnum +from typing import ( + TYPE_CHECKING, + Any, + Generic, + cast, +) + +from typing_extensions import Self + +import temporalio.api.activity.v1 +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.converter._search_attributes +from temporalio.converter import ( + ActivitySerializationContext, + DataConverter, + SerializationContext, + WithSerializationContext, +) +from temporalio.service import ( + RPCError, + RPCStatusCode, +) + +from ..types import ( + ReturnType, +) +from ._exceptions import ActivityFailureError +from ._interceptor import ( + CancelActivityInput, + CompleteAsyncActivityInput, + DescribeActivityInput, + FailAsyncActivityInput, + HeartbeatAsyncActivityInput, + ReportCancellationAsyncActivityInput, + TerminateActivityInput, +) + +if TYPE_CHECKING: + from ._client import Client + from ._interceptor import ListActivitiesInput + + +class ActivityExecutionAsyncIterator: + """Asynchronous iterator for activity execution values. + + You should typically use ``async for`` on this iterator and not call any of its methods. + + .. warning:: + This API is experimental. + """ + + def __init__( + self, + client: Client, + input: ListActivitiesInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_activities`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[ActivityExecution] | None = None + self._current_page_index = 0 + self._limit = input.limit + self._yielded = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[ActivityExecution] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page of results. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + page_size = page_size or self._input.page_size + if self._limit is not None and self._limit - self._yielded < page_size: + page_size = self._limit - self._yielded + + resp = await self._client.workflow_service.list_activity_executions( + temporalio.api.workflowservice.v1.ListActivityExecutionsRequest( + namespace=self._client.namespace, + page_size=page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + + self._current_page = [ + ActivityExecution._from_raw_info(v, self._client.namespace) + for v in resp.executions + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> ActivityExecutionAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> ActivityExecution: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + if self._limit is not None and self._yielded >= self._limit: + raise StopAsyncIteration + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + self._yielded += 1 + return ret + + +@dataclass(frozen=True) +class ActivityExecution: + """Info for an activity execution not started by a workflow, from list response. + + .. warning:: + This API is experimental. + """ + + activity_id: str + """Activity ID.""" + + activity_run_id: str | None + """Run ID of the activity.""" + + activity_type: str + """Type name of the activity.""" + + close_time: datetime | None + """Time the activity reached a terminal status, if closed.""" + + execution_duration: timedelta | None + """Duration from scheduled to close time, only populated if closed.""" + + namespace: str + """Namespace of the activity (copied from calling client).""" + + raw_info: ( + temporalio.api.activity.v1.ActivityExecutionListInfo + | temporalio.api.activity.v1.ActivityExecutionInfo + ) + """Underlying protobuf info.""" + + scheduled_time: datetime + """Time the activity was originally scheduled.""" + + state_transition_count: int | None + """Number of state transitions, if available.""" + + status: ActivityExecutionStatus + """Current status of the activity.""" + + task_queue: str + """Task queue the activity was scheduled on.""" + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Current set of search attributes if any.""" + + @classmethod + def _from_raw_info( + cls, info: temporalio.api.activity.v1.ActivityExecutionListInfo, namespace: str + ) -> Self: + """Create from raw proto activity list info.""" + return cls( + activity_id=info.activity_id, + activity_run_id=info.run_id or None, + activity_type=( + info.activity_type.name if info.HasField("activity_type") else "" + ), + close_time=( + info.close_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + execution_duration=( + info.execution_duration.ToTimedelta() + if info.HasField("execution_duration") + else None + ), + namespace=namespace, + raw_info=info, + scheduled_time=( + info.schedule_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("schedule_time") + else datetime.min + ), + state_transition_count=( + info.state_transition_count if info.state_transition_count else None + ), + status=( + ActivityExecutionStatus(info.status) + if info.status + else ActivityExecutionStatus.UNSPECIFIED + ), + task_queue=info.task_queue, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + ) + + +@dataclass(frozen=True) +class ActivityExecutionDescription(ActivityExecution): + """Detailed information about an activity execution not started by a workflow. + + .. warning:: + This API is experimental. + """ + + attempt: int + """Current attempt number.""" + + canceled_reason: str | None + """Reason for cancellation, if cancel was requested.""" + + current_retry_interval: timedelta | None + """Time until the next retry, if applicable.""" + + eager_execution_requested: bool + """Whether eager execution was requested for this activity.""" + + expiration_time: datetime + """Scheduled time plus schedule_to_close_timeout.""" + + last_attempt_complete_time: datetime | None + """Time when the last attempt completed.""" + + last_failure: Exception | None + """Failure from the last failed attempt, if any.""" + + last_heartbeat_time: datetime | None + """Time of the last heartbeat.""" + + last_started_time: datetime | None + """Time the last attempt was started.""" + + last_worker_identity: str + """Identity of the last worker that processed the activity.""" + + next_attempt_schedule_time: datetime | None + """Time when the next attempt will be scheduled.""" + + paused: bool + """Whether the activity is paused.""" + + raw_heartbeat_details: Sequence[temporalio.api.common.v1.Payload] + """Details from the last heartbeat.""" + + retry_policy: temporalio.common.RetryPolicy | None + """Retry policy for the activity.""" + + run_state: PendingActivityState | None + """More detailed breakdown if status is RUNNING.""" + + long_poll_token: bytes | None + """Token for follow-on long-poll requests. None if the activity is complete.""" + + @classmethod + async def _from_execution_info( + cls, + info: temporalio.api.activity.v1.ActivityExecutionInfo, + long_poll_token: bytes | None, + namespace: str, + data_converter: temporalio.converter.DataConverter, + ) -> Self: + """Create from raw proto activity execution info.""" + # Decode heartbeat details if present + decoded_heartbeat_details: Sequence[temporalio.api.common.v1.Payload] = ( + info.heartbeat_details.payloads + ) + if decoded_heartbeat_details and data_converter.payload_codec: + decoded_heartbeat_details = await data_converter.payload_codec.decode( + decoded_heartbeat_details + ) + + return cls( + activity_id=info.activity_id, + activity_run_id=info.run_id or None, + activity_type=( + info.activity_type.name if info.HasField("activity_type") else "" + ), + attempt=info.attempt, + canceled_reason=info.canceled_reason or None, + close_time=( + info.close_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + current_retry_interval=( + info.current_retry_interval.ToTimedelta() + if info.HasField("current_retry_interval") + else None + ), + eager_execution_requested=getattr(info, "eager_execution_requested", False), + execution_duration=( + info.execution_duration.ToTimedelta() + if info.HasField("execution_duration") + else None + ), + expiration_time=( + info.expiration_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("expiration_time") + else datetime.min + ), + last_attempt_complete_time=( + info.last_attempt_complete_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_attempt_complete_time") + else None + ), + last_failure=( + cast( + Exception | None, + await data_converter.decode_failure(info.last_failure), + ) + if info.HasField("last_failure") + else None + ), + last_heartbeat_time=( + info.last_heartbeat_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_heartbeat_time") + else None + ), + last_started_time=( + info.last_started_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("last_started_time") + else None + ), + last_worker_identity=info.last_worker_identity, + long_poll_token=long_poll_token or None, + namespace=namespace, + next_attempt_schedule_time=( + info.next_attempt_schedule_time.ToDatetime(tzinfo=timezone.utc) + if info.HasField("next_attempt_schedule_time") + else None + ), + paused=getattr(info, "paused", False), + raw_heartbeat_details=decoded_heartbeat_details, + raw_info=info, + retry_policy=temporalio.common.RetryPolicy.from_proto(info.retry_policy) + if info.HasField("retry_policy") + else None, + run_state=( + PendingActivityState(info.run_state) if info.run_state else None + ), + scheduled_time=(info.schedule_time.ToDatetime(tzinfo=timezone.utc)), + state_transition_count=( + info.state_transition_count if info.state_transition_count else None + ), + status=( + ActivityExecutionStatus(info.status) + if info.status + else ActivityExecutionStatus.UNSPECIFIED + ), + task_queue=info.task_queue, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + ) + + +class ActivityExecutionStatus(IntEnum): + """Status of an activity execution. + + .. warning:: + This API is experimental. + + See :py:class:`temporalio.api.enums.v1.ActivityExecutionStatus`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_UNSPECIFIED + ) + RUNNING = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_RUNNING + ) + COMPLETED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_COMPLETED + ) + FAILED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_FAILED + ) + CANCELED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_CANCELED + ) + TERMINATED = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_TERMINATED + ) + TIMED_OUT = int( + temporalio.api.enums.v1.ActivityExecutionStatus.ACTIVITY_EXECUTION_STATUS_TIMED_OUT + ) + + +class PendingActivityState(IntEnum): + """Detailed state of an activity execution that is in ACTIVITY_EXECUTION_STATUS_RUNNING. + + .. warning:: + This API is experimental. + + See :py:class:`temporalio.api.enums.v1.PendingActivityState`. + """ + + UNSPECIFIED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_UNSPECIFIED + ) + SCHEDULED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_SCHEDULED + ) + STARTED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_STARTED + ) + CANCEL_REQUESTED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_CANCEL_REQUESTED + ) + PAUSED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_PAUSED + ) + PAUSE_REQUESTED = int( + temporalio.api.enums.v1.PendingActivityState.PENDING_ACTIVITY_STATE_PAUSE_REQUESTED + ) + + +@dataclass(frozen=True) +class ActivityExecutionCount: + """Representation of a count from a count activities call. + + .. warning:: + This API is experimental. + """ + + count: int + """Total count matching the filter, if any.""" + + groups: Sequence[ActivityExecutionCountAggregationGroup] + """Aggregation groups if requested.""" + + @staticmethod + def _from_raw( + resp: temporalio.api.workflowservice.v1.CountActivityExecutionsResponse, + ) -> ActivityExecutionCount: + """Create from raw proto response.""" + return ActivityExecutionCount( + count=resp.count, + groups=[ + ActivityExecutionCountAggregationGroup._from_raw(g) for g in resp.groups + ], + ) + + +@dataclass(frozen=True) +class ActivityExecutionCountAggregationGroup: + """A single aggregation group from a count activities call. + + .. warning:: + This API is experimental. + """ + + count: int + """Count for this group.""" + + group_values: Sequence[temporalio.common.SearchAttributeValue] + """Values that define this group.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountActivityExecutionsResponse.AggregationGroup, + ) -> ActivityExecutionCountAggregationGroup: + return ActivityExecutionCountAggregationGroup( + count=raw.count, + group_values=[ + temporalio.converter._search_attributes._decode_search_attribute_value( + v + ) + for v in raw.group_values + ], + ) + + +@dataclass(frozen=True) +class AsyncActivityIDReference: + """Reference to an async activity by its qualified ID.""" + + workflow_id: str | None + run_id: str | None + activity_id: str + + +class AsyncActivityHandle(WithSerializationContext): + """Handle representing an external activity for completion and heartbeat.""" + + def __init__( + self, + client: Client, + id_or_token: AsyncActivityIDReference | bytes, + data_converter_override: DataConverter | None = None, + ) -> None: + """Create an async activity handle.""" + self._client = client + self._id_or_token = id_or_token + self._data_converter_override = data_converter_override + + async def heartbeat( + self, + *details: Any, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Record a heartbeat for the activity. + + Args: + details: Details of the heartbeat. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.heartbeat_async_activity( + HeartbeatAsyncActivityInput( + id_or_token=self._id_or_token, + details=details, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + async def complete( + self, + result: Any | None = temporalio.common._arg_unset, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Complete the activity. + + Args: + result: Result of the activity if any. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.complete_async_activity( + CompleteAsyncActivityInput( + id_or_token=self._id_or_token, + result=result, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + async def fail( + self, + error: Exception, + *, + last_heartbeat_details: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Fail the activity. + + Args: + error: Error for the activity. + last_heartbeat_details: Last heartbeat details for the activity. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.fail_async_activity( + FailAsyncActivityInput( + id_or_token=self._id_or_token, + error=error, + last_heartbeat_details=last_heartbeat_details, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + async def report_cancellation( + self, + *details: Any, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Report the activity as cancelled. + + Args: + details: Cancellation details. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.report_cancellation_async_activity( + ReportCancellationAsyncActivityInput( + id_or_token=self._id_or_token, + details=details, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + data_converter_override=self._data_converter_override, + ), + ) + + def with_context(self, context: SerializationContext) -> Self: + """Create a new AsyncActivityHandle with a different serialization context. + + Payloads received by the activity will be decoded and deserialized using a data converter + with :py:class:`ActivitySerializationContext` set as context. If you are using a custom data + converter that makes use of this context then you can use this method to supply matching + context data to the data converter used to serialize and encode the outbound payloads. + """ + data_converter = self._client.data_converter.with_context(context) + if data_converter is self._client.data_converter: + return self + cls = type(self) + if cls.__init__ is not AsyncActivityHandle.__init__: + raise TypeError( + "If you have subclassed AsyncActivityHandle and overridden the __init__ method " + "then you must override with_context to return an instance of your class." + ) + return cls( + self._client, + self._id_or_token, + data_converter, + ) + + +class ActivityHandle(Generic[ReturnType]): + """Handle representing an activity execution not started by a workflow. + + .. warning:: + This API is experimental. + """ + + def __init__( + self, + client: Client, + id: str, + *, + run_id: str | None = None, + result_type: type | None = None, + ) -> None: + """Create activity handle.""" + self._client = client + self._id = id + self._run_id = run_id + self._result_type = result_type + self._known_outcome: ( + temporalio.api.activity.v1.ActivityExecutionOutcome | None + ) = None + + @functools.cached_property + def _data_converter(self) -> temporalio.converter.DataConverter: + return self._client.data_converter.with_context( + ActivitySerializationContext( + namespace=self._client.namespace, + activity_id=self._id, + activity_type=None, + activity_task_queue=None, + is_local=False, + workflow_id=None, + workflow_type=None, + ) + ) + + @property + def id(self) -> str: + """ID of the activity.""" + return self._id + + @property + def run_id(self) -> str | None: + """Run ID of the activity.""" + return self._run_id + + async def result( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Wait for result of the activity. + + .. warning:: + This API is experimental. + + The result may already be known if this method has been called before, + in which case no network call is made. Otherwise the result will be + polled for until it is available. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note: + this is the timeout for each RPC call while polling, not a + timeout for the function as a whole. If an individual RPC + times out, it will be retried until the result is available. + + Returns: + The result of the activity. + + Raises: + ActivityFailureError: If the activity completed with a failure. + RPCError: Activity result could not be fetched for some reason. + """ + await self._poll_until_outcome( + rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + + # Convert outcome to failure or value + assert self._known_outcome + if self._known_outcome.HasField("failure"): + raise ActivityFailureError( + cause=await self._data_converter.decode_failure( + self._known_outcome.failure + ), + ) + if not self._known_outcome.result.payloads: + return None # type: ignore + type_hints = [self._result_type] if self._result_type else None + results = await self._data_converter.decode( + self._known_outcome.result.payloads, type_hints + ) + if not results: + return None # type: ignore + elif len(results) > 1: + warnings.warn(f"Expected single activity result, got {len(results)}") + return results[0] + + async def _poll_until_outcome( + self, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Poll for activity result until it's available.""" + if self._known_outcome: + return + + req = temporalio.api.workflowservice.v1.PollActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=self._id, + run_id=self._run_id or "", + ) + + # Continue polling as long as we have no outcome + while True: + try: + res = await self._client.workflow_service.poll_activity_execution( + req, + retry=True, + metadata=rpc_metadata, + timeout=rpc_timeout, + ) + if res.HasField("outcome"): + self._known_outcome = res.outcome + return + except RPCError as err: + if err.status == RPCStatusCode.DEADLINE_EXCEEDED: + # Deadline exceeded is expected with long polling; retry + continue + elif err.status == RPCStatusCode.CANCELLED: + raise asyncio.CancelledError() from err + else: + raise + except asyncio.CancelledError: + raise + + async def cancel( + self, + *, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Request cancellation of the activity. + + .. warning:: + This API is experimental. + + Requesting cancellation of an activity does not automatically transition the activity to + canceled status. If the activity is heartbeating, a :py:class:`exceptions.CancelledError` + exception will be raised when receiving the heartbeat response; if the activity allows this + exception to bubble out, the activity will transition to canceled status. If the activity it + is not heartbeating, this method will have no effect on activity status. + + Args: + reason: Reason for the cancellation. Recorded and available via describe. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.cancel_activity( + CancelActivityInput( + activity_id=self._id, + activity_run_id=self._run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def terminate( + self, + *, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Terminate the activity execution immediately. + + .. warning:: + This API is experimental. + + Termination does not reach the worker and the activity code cannot react to it. + A terminated activity may have a running attempt and will be requested to be + canceled by the server when it heartbeats. + + Args: + reason: Reason for the termination. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.terminate_activity( + TerminateActivityInput( + activity_id=self._id, + activity_run_id=self._run_id, + reason=reason, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def describe( + self, + *, + long_poll_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityExecutionDescription: + """Describe the activity execution. + + .. warning:: + This API is experimental. + + Args: + long_poll_token: Token from a previous describe response. If provided, + the request will long-poll until the activity state changes. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Activity execution description. + """ + return await self._client._impl.describe_activity( + DescribeActivityInput( + activity_id=self._id, + activity_run_id=self._run_id, + long_poll_token=long_poll_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) diff --git a/temporalio/client/_callback.py b/temporalio/client/_callback.py new file mode 100644 index 000000000..45aa89030 --- /dev/null +++ b/temporalio/client/_callback.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +import temporalio.nexus + +Callback = temporalio.nexus.NexusCallback diff --git a/temporalio/client/_client.py b/temporalio/client/_client.py new file mode 100644 index 000000000..437c9f2b2 --- /dev/null +++ b/temporalio/client/_client.py @@ -0,0 +1,2898 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from collections.abc import ( + Awaitable, + Callable, + Mapping, + Sequence, +) +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + cast, + overload, +) + +from typing_extensions import Required, Self, TypedDict + +import temporalio.activity +import temporalio.api.common.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.runtime +import temporalio.service +import temporalio.workflow +from temporalio.service import ( + ConnectConfig, + DnsLoadBalancingConfig, + HttpConnectProxyConfig, + KeepAliveConfig, + RetryConfig, + ServiceClient, + TLSConfig, +) + +from ..common import HeaderCodecBehavior +from ..types import ( + CallableAsyncNoParam, + CallableAsyncSingleParam, + CallableSyncNoParam, + CallableSyncSingleParam, + LocalReturnType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._activity import ( + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityHandle, + AsyncActivityHandle, + AsyncActivityIDReference, +) +from ._callback import Callback +from ._impl import _ClientImpl +from ._interceptor import ( + CountActivitiesInput, + CountWorkflowsInput, + CreateScheduleInput, + GetWorkerBuildIdCompatibilityInput, + GetWorkerTaskReachabilityInput, + ListActivitiesInput, + ListSchedulesInput, + ListWorkflowsInput, + OutboundInterceptor, + StartActivityInput, + StartWorkflowInput, + StartWorkflowUpdateWithStartInput, + UpdateWithStartUpdateWorkflowInput, + UpdateWorkerBuildIdCompatibilityInput, +) +from ._schedule import ( + Schedule, + ScheduleAsyncIterator, + ScheduleBackfill, + ScheduleHandle, +) +from ._worker_versioning import ( + BuildIdOp, + TaskReachabilityType, + WorkerBuildIdVersionSets, + WorkerTaskReachability, +) +from ._workflow import ( + WithStartWorkflowOperation, + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowHandle, + WorkflowUpdateHandle, + WorkflowUpdateStage, +) + +if TYPE_CHECKING: + from ._interceptor import Interceptor + from ._plugin import Plugin + + +class Client: + """Client for accessing Temporal. + + Most users will use :py:meth:`connect` to create a client. The + :py:attr:`service` property provides access to a raw gRPC client. To create + another client, like for a different namespace, :py:func:`Client` may be + directly instantiated with a :py:attr:`service` of another. + + Clients are not thread-safe and should only be used in the event loop they + are first connected in. If a client needs to be used from another thread + than where it was created, make sure the event loop where it was created is + captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the + client call and that event loop. + + Clients do not work across forks since runtimes do not work across forks. + """ + + @classmethod + async def connect( + cls, + target_host: str, + *, + namespace: str = "default", + api_key: str | None = None, + data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, + plugins: Sequence[Plugin] = [], + interceptors: Sequence[Interceptor] = [], + default_workflow_query_reject_condition: None + | (temporalio.common.QueryRejectCondition) = None, + tls: bool | TLSConfig | None = None, + retry_config: RetryConfig | None = None, + keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + identity: str | None = None, + lazy: bool = False, + runtime: temporalio.runtime.Runtime | None = None, + http_connect_proxy_config: HttpConnectProxyConfig | None = None, + dns_load_balancing_config: DnsLoadBalancingConfig | None = None, + header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, + ) -> Self: + """Connect to a Temporal server. + + Args: + target_host: ``host:port`` for the Temporal server. For local + development, this is often "localhost:7233". + namespace: Namespace to use for client calls. + api_key: API key for Temporal. This becomes the "Authorization" + HTTP header with "Bearer " prepended. This is only set if RPC + metadata doesn't already have an "authorization" key. + data_converter: Data converter to use for all data conversions + to/from payloads. + plugins: Set of plugins that are chained together to allow + intercepting and modifying client creation and service connection. + The earlier plugins wrap the later ones. + + Any plugins that also implement + :py:class:`temporalio.worker.Plugin` will be used as worker + plugins too so they should not be given when creating a + worker. + interceptors: Set of interceptors that are chained together to allow + intercepting of client calls. The earlier interceptors wrap the + later ones. + + Any interceptors that also implement + :py:class:`temporalio.worker.Interceptor` will be used as worker + interceptors too so they should not be given when creating a + worker. + default_workflow_query_reject_condition: The default rejection + condition for workflow queries if not set during query. See + :py:meth:`WorkflowHandle.query` for details on the rejection + condition. + tls: If ``None``, the default, TLS will be enabled automatically + when ``api_key`` is provided, otherwise TLS is disabled. If + ``False``, do not use TLS. If ``True``, use system default TLS + configuration. If TLS configuration present, that TLS + configuration will be used. + retry_config: Retry configuration for direct service calls (when + opted in) or all high-level calls made by this client (which all + opt-in to retries by default). If unset, a default retry + configuration is used. + keep_alive_config: Keep-alive configuration for the client + connection. Default is to check every 30s and kill the + connection if a response doesn't come back in 15s. Can be set to + ``None`` to disable. + rpc_metadata: Headers to use for all calls to the server. Keys here + can be overriden by per-call RPC metadata keys. + identity: Identity for this client. If unset, a default is created + based on the version of the SDK. + lazy: If true, the client will not connect until the first call is + attempted or a worker is created with it. Lazy clients cannot be + used for workers. + runtime: The runtime for this client, or the default if unset. + http_connect_proxy_config: Configuration for HTTP CONNECT proxy. + dns_load_balancing_config: DNS load balancing configuration for the + client connection. Default is to re-resolve DNS every 30s. Can + be set to ``None`` to disable. Silently disabled when + ``http_connect_proxy_config`` is set, since the two are mutually + exclusive. + header_codec_behavior: Encoding behavior for headers sent by the client. + """ + connect_config = temporalio.service.ConnectConfig( + target_host=target_host, + api_key=api_key, + tls=tls, + retry_config=retry_config, + keep_alive_config=keep_alive_config, + rpc_metadata=rpc_metadata, + identity=identity or "", + lazy=lazy, + runtime=runtime, + http_connect_proxy_config=http_connect_proxy_config, + dns_load_balancing_config=dns_load_balancing_config, + ) + + def make_lambda( + plugin: Plugin, next: Callable[[ConnectConfig], Awaitable[ServiceClient]] + ): + return lambda config: plugin.connect_service_client(config, next) + + next_function = ServiceClient.connect + for plugin in reversed(plugins): + next_function = make_lambda(plugin, next_function) + + service_client = await next_function(connect_config) + + return cls( + service_client, + namespace=namespace, + data_converter=data_converter, + interceptors=interceptors, + default_workflow_query_reject_condition=default_workflow_query_reject_condition, + header_codec_behavior=header_codec_behavior, + plugins=plugins, + ) + + def __init__( + self, + service_client: temporalio.service.ServiceClient, + *, + namespace: str = "default", + data_converter: temporalio.converter.DataConverter = temporalio.converter.DataConverter.default, + plugins: Sequence[Plugin] = [], + interceptors: Sequence[Interceptor] = [], + default_workflow_query_reject_condition: None + | (temporalio.common.QueryRejectCondition) = None, + header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC, + ): + """Create a Temporal client from a service client. + + See :py:meth:`connect` for details on the parameters. + """ + # Store the config for tracking + config = ClientConfig( + service_client=service_client, + namespace=namespace, + data_converter=data_converter, + plugins=plugins, + interceptors=interceptors, + default_workflow_query_reject_condition=default_workflow_query_reject_condition, + header_codec_behavior=header_codec_behavior, + ) + self._initial_config = config.copy() + + for plugin in plugins: + config = plugin.configure_client(config) + + self._init_from_config(config) + + def _init_from_config(self, config: ClientConfig): + self._config = config + + # Iterate over interceptors in reverse building the impl + self._impl: OutboundInterceptor = _ClientImpl(self) + for interceptor in reversed(list(self._config["interceptors"])): + self._impl = interceptor.intercept_client(self._impl) + + def config(self, *, active_config: bool = False) -> ClientConfig: + """Config, as a dictionary, used to create this client. + + Args: + active_config: If true, return the modified configuration in use rather than the initial one + provided to the client. + + This makes a shallow copy of the config each call. + """ + config = self._config.copy() if active_config else self._initial_config.copy() + config["interceptors"] = list(config["interceptors"]) + return config + + @property + def service_client(self) -> temporalio.service.ServiceClient: + """Raw gRPC service client.""" + return self._config["service_client"] + + @property + def workflow_service(self) -> temporalio.service.WorkflowService: + """Raw gRPC workflow service client.""" + return self._config["service_client"].workflow_service + + @property + def operator_service(self) -> temporalio.service.OperatorService: + """Raw gRPC operator service client.""" + return self._config["service_client"].operator_service + + @property + def test_service(self) -> temporalio.service.TestService: + """Raw gRPC test service client.""" + return self._config["service_client"].test_service + + @property + def namespace(self) -> str: + """Namespace used in calls by this client.""" + return self._config["namespace"] + + @property + def identity(self) -> str: + """Identity used in calls by this client.""" + return self._config["service_client"].config.identity + + @property + def data_converter(self) -> temporalio.converter.DataConverter: + """Data converter used by this client.""" + return self._config["data_converter"] + + @property + def rpc_metadata(self) -> Mapping[str, str | bytes]: + """Headers for every call made by this client. + + Do not use mutate this mapping. Rather, set this property with an + entirely new mapping to change the headers. + """ + return self.service_client.config.rpc_metadata + + @rpc_metadata.setter + def rpc_metadata(self, value: Mapping[str, str | bytes]) -> None: + """Update the headers for this client. + + Do not mutate this mapping after set. Rather, set an entirely new + mapping if changes are needed. + + Raises: + TypeError: the key/value pair is not a valid gRPC ASCII or binary metadata. + All binary metadata must be supplied as bytes, and the key must end in '-bin'. + + .. warning:: + Attempting to set an invalid binary RPC metadata value may leave the client + in an inconsistent state (as well as raise a :py:class:`TypeError`). + """ + # Update config and perform update + # This may raise if the metadata is invalid: + self.service_client.update_rpc_metadata(value) + self.service_client.config.rpc_metadata = value + + @property + def api_key(self) -> str | None: + """API key for every call made by this client.""" + return self.service_client.config.api_key + + @api_key.setter + def api_key(self, value: str | None) -> None: + """Update the API key for this client. + + This is only set if RPCmetadata doesn't already have an "authorization" + key. + """ + # Update config and perform update + self.service_client.config.api_key = value + self.service_client.update_api_key(value) + + # Overload for no-param workflow + @overload + async def start_workflow( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: ... + + # Overload for single-param workflow + @overload + async def start_workflow( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: ... + + # Overload for multi-param workflow + @overload + async def start_workflow( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: ... + + # Overload for string-name workflow + @overload + async def start_workflow( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> WorkflowHandle[Any, Any]: ... + + async def start_workflow( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + # The following options should not be considered part of the public API. They + # are deliberately not exposed in overloads, and are not subject to any + # backwards compatibility guarantees. + callbacks: Sequence[Callback] = [], + workflow_event_links: Sequence[ + temporalio.api.common.v1.Link.WorkflowEvent + ] = [], + request_id: str | None = None, + stack_level: int = 2, + ) -> WorkflowHandle[Any, Any]: + """Start a workflow and return its handle. + + Args: + workflow: String name or class method decorated with + ``@workflow.run`` for the workflow to start. + arg: Single argument to the workflow. + args: Multiple arguments to the workflow. Cannot be set if arg is. + id: Unique identifier for the workflow execution. + task_queue: Task queue to run the workflow on. + result_type: For string workflows, this can set the specific result + type hint to deserialize into. + execution_timeout: Total workflow execution timeout including + retries and continue as new. + run_timeout: Timeout of a single workflow run. + task_timeout: Timeout of a single workflow task. + id_conflict_policy: Behavior when a workflow is currently running with the same ID. + Default is UNSPECIFIED, which effectively means fail the start attempt. + Set to USE_EXISTING for idempotent deduplication on workflow ID. + Cannot be set if ``id_reuse_policy`` is set to TERMINATE_IF_RUNNING. + id_reuse_policy: Behavior when a closed workflow with the same ID exists. + Default is ALLOW_DUPLICATE. + retry_policy: Retry policy for the workflow. + cron_schedule: See https://docs.temporal.io/docs/content/what-is-a-temporal-cron-job/ + memo: Memo for the workflow. + search_attributes: Search attributes for the workflow. The + dictionary form of this is deprecated, use + :py:class:`temporalio.common.TypedSearchAttributes`. + static_summary: A single-line fixed summary for this workflow execution that may appear + in the UI/CLI. This can be in single-line Temporal markdown format. + static_details: General fixed details for this workflow execution that may appear in + UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is + a fixed value on the workflow that cannot be updated. For details that can be + updated, use :py:meth:`temporalio.workflow.get_current_details` within the workflow. + start_delay: Amount of time to wait before starting the workflow. + This does not work with ``cron_schedule``. + start_signal: If present, this signal is sent as signal-with-start + instead of traditional workflow start. + start_signal_args: Arguments for start_signal if start_signal + present. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + request_eager_start: Potentially reduce the latency to start this workflow by + encouraging the server to start it on a local worker running with + this same client. + priority: Priority of the workflow execution. + versioning_override: Overrides the versioning behavior for this workflow. + + Returns: + A workflow handle to the started workflow. + + Raises: + temporalio.exceptions.WorkflowAlreadyStartedError: Workflow has + already been started. + RPCError: Workflow could not be started for some other reason. + """ + temporalio.common._warn_on_deprecated_search_attributes( + search_attributes, stack_level=stack_level + ) + name, result_type_from_type_hint = ( + temporalio.workflow._Definition.get_name_and_result_type(workflow) + ) + return await self._impl.start_workflow( + StartWorkflowInput( + workflow=name, + args=temporalio.common._arg_or_args(arg, args), + id=id, + task_queue=task_queue, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + start_delay=start_delay, + versioning_override=versioning_override, + headers={}, + static_summary=static_summary, + static_details=static_details, + start_signal=start_signal, + start_signal_args=start_signal_args, + ret_type=result_type or result_type_from_type_hint, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + callbacks=callbacks, + workflow_event_links=workflow_event_links, + request_id=request_id, + ) + ) + + # Overload for no-param workflow + @overload + async def execute_workflow( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> ReturnType: ... + + # Overload for single-param workflow + @overload + async def execute_workflow( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> ReturnType: ... + + # Overload for multi-param workflow + @overload + async def execute_workflow( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> ReturnType: ... + + # Overload for string-name workflow + @overload + async def execute_workflow( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> Any: ... + + async def execute_workflow( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy = temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + start_signal: str | None = None, + start_signal_args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + request_eager_start: bool = False, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> Any: + """Start a workflow and wait for completion. + + This is a shortcut for :py:meth:`start_workflow` + + :py:meth:`WorkflowHandle.result`. + """ + return await ( + # We have to tell MyPy to ignore errors here because we want to call + # the non-@overload form of this and MyPy does not support that + await self.start_workflow( # type: ignore + workflow, # type: ignore[arg-type] + arg, + args=args, + task_queue=task_queue, + result_type=result_type, + id=id, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + start_signal=start_signal, + start_signal_args=start_signal_args, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + request_eager_start=request_eager_start, + priority=priority, + versioning_override=versioning_override, + stack_level=3, + ) + ).result() + + def get_workflow_handle( + self, + workflow_id: str, + *, + run_id: str | None = None, + first_execution_run_id: str | None = None, + result_type: type | None = None, + ) -> WorkflowHandle[Any, Any]: + """Get a workflow handle to an existing workflow by its ID. + + Args: + workflow_id: Workflow ID to get a handle to. + run_id: Run ID that will be used for all calls. + first_execution_run_id: First execution run ID used for cancellation + and termination. + result_type: The result type to deserialize into if known. + + Returns: + The workflow handle. + """ + return WorkflowHandle( + self, + workflow_id, + run_id=run_id, + result_run_id=run_id, + first_execution_run_id=first_execution_run_id, + result_type=result_type, + ) + + def get_workflow_handle_for( + self, + workflow: ( + MethodAsyncNoParam[SelfType, ReturnType] + | MethodAsyncSingleParam[SelfType, Any, ReturnType] + ), + workflow_id: str, + *, + run_id: str | None = None, + first_execution_run_id: str | None = None, + ) -> WorkflowHandle[SelfType, ReturnType]: + """Get a typed workflow handle to an existing workflow by its ID. + + This is the same as :py:meth:`get_workflow_handle` but typed. + + Args: + workflow: The workflow run method to use for typing the handle. + workflow_id: Workflow ID to get a handle to. + run_id: Run ID that will be used for all calls. + first_execution_run_id: First execution run ID used for cancellation + and termination. + + Returns: + The workflow handle. + """ + defn = temporalio.workflow._Definition.must_from_run_fn(workflow) + return self.get_workflow_handle( + workflow_id, + run_id=run_id, + first_execution_run_id=first_execution_run_id, + result_type=defn.ret_type, + ) + + # Overload for no-param update + @overload + async def execute_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for single-param update + @overload + async def execute_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for multi-param update + @overload + async def execute_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for string-name update + @overload + async def execute_update_with_start_workflow( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def execute_update_with_start_workflow( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Send an update-with-start request and wait for the update to complete. + + A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the + specified workflow execution is not running, a new workflow execution is started + and the update is sent in the first workflow task. Alternatively if the specified + workflow execution is running then, if the WorkflowIDConflictPolicy is + USE_EXISTING, the update is issued against the specified workflow, and if the + WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until + the update has completed, and return the update result. Note that this means that + the call will not return successfully until the update has been delivered to a + worker. + + Args: + update: Update function or name on the workflow. arg: Single argument to the + update. + args: Multiple arguments to the update. Cannot be set if arg is. + start_workflow_operation: a WithStartWorkflowOperation definining the + WorkflowIDConflictPolicy and how to start the workflow in the event that a + workflow is started. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed out or + cancelled. + + RPCError: There was some issue starting the workflow or sending the update to + the workflow. + """ + handle = await self._start_update_with_start( + update, + arg, + args=args, + start_workflow_operation=start_workflow_operation, + wait_for_stage=WorkflowUpdateStage.COMPLETED, + id=id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + # Overload for no-param start update + @overload + async def start_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for single-param start update + @overload + async def start_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for multi-param start update + @overload + async def start_update_with_start_workflow( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + start_workflow_operation: WithStartWorkflowOperation[SelfType, Any], + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for string-name start update + @overload + async def start_update_with_start_workflow( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: ... + + async def start_update_with_start_workflow( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + start_workflow_operation: WithStartWorkflowOperation[Any, Any], + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + """Send an update-with-start request and wait for it to be accepted. + + A WorkflowIDConflictPolicy must be set in the start_workflow_operation. If the + specified workflow execution is not running, a new workflow execution is started + and the update is sent in the first workflow task. Alternatively if the specified + workflow execution is running then, if the WorkflowIDConflictPolicy is + USE_EXISTING, the update is issued against the specified workflow, and if the + WorkflowIDConflictPolicy is FAIL, an error is returned. This call will block until + the update has been accepted, and return a WorkflowUpdateHandle. Note that this + means that the call will not return successfully until the update has been + delivered to a worker. + + Args: + update: Update function or name on the workflow. arg: Single argument to the + update. + args: Multiple arguments to the update. Cannot be set if arg is. + start_workflow_operation: a WithStartWorkflowOperation definining the + WorkflowIDConflictPolicy and how to start the workflow in the event that a + workflow is started. + wait_for_stage: Required stage to wait until returning: either ACCEPTED or + COMPLETED. ADMITTED is not currently supported. See + https://docs.temporal.io/workflows#update for more details. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed out or + cancelled. + + RPCError: There was some issue starting the workflow or sending the update to + the workflow. + """ + return await self._start_update_with_start( + update, + arg, + wait_for_stage=wait_for_stage, + args=args, + id=id, + result_type=result_type, + start_workflow_operation=start_workflow_operation, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + async def _start_update_with_start( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + start_workflow_operation: WithStartWorkflowOperation[SelfType, ReturnType], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + if wait_for_stage == WorkflowUpdateStage.ADMITTED: + raise ValueError("ADMITTED wait stage not supported") + + if start_workflow_operation._used: + raise RuntimeError("WithStartWorkflowOperation cannot be reused") + start_workflow_operation._used = True + + update_name, result_type_from_type_hint = ( + temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) + ) + + update_input = UpdateWithStartUpdateWorkflowInput( + update_id=id, + update=update_name, + args=temporalio.common._arg_or_args(arg, args), + headers={}, + ret_type=result_type or result_type_from_type_hint, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + wait_for_stage=wait_for_stage, + ) + + def on_start( + start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, + ): + start_workflow_operation._workflow_handle.set_result( + WorkflowHandle( + self, + start_workflow_operation._start_workflow_input.id, + first_execution_run_id=start_response.run_id, + result_run_id=start_response.run_id, + result_type=start_workflow_operation._start_workflow_input.ret_type, + ) + ) + + def on_start_error( + error: BaseException, + ): + start_workflow_operation._workflow_handle.set_exception(error) + + input = StartWorkflowUpdateWithStartInput( + start_workflow_input=start_workflow_operation._start_workflow_input, + update_workflow_input=update_input, + _on_start=on_start, + _on_start_error=on_start_error, + ) + + return await self._impl.start_update_with_start_workflow(input) + + def list_workflows( + self, + query: str | None = None, + *, + limit: int | None = None, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowExecutionAsyncIterator: + """List workflows. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + query: A Temporal visibility list filter. See Temporal documentation + concerning visibility list filters including behavior when left + unset. + limit: Maximum number of workflows to return. If unset, all + workflows are returned. Only applies if using the + returned :py:class:`WorkflowExecutionAsyncIterator`. + as an async iterator. + page_size: Maximum number of results for each page. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_workflows( + ListWorkflowsInput( + query=query, + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + limit=limit, + ) + ) + + async def count_workflows( + self, + query: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowExecutionCount: + """Count workflows. + + Args: + query: A Temporal visibility filter. See Temporal documentation + concerning visibility list filters. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + Count of workflows. + """ + return await self._impl.count_workflows( + CountWorkflowsInput( + query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + ) + + # async no-param + @overload + async def start_activity( + self, + activity: CallableAsyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync no-param + @overload + async def start_activity( + self, + activity: CallableSyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async single-param + @overload + async def start_activity( + self, + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync single-param + @overload + async def start_activity( + self, + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async multi-param + @overload + async def start_activity( + self, + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync multi-param + @overload + async def start_activity( + self, + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # string name + @overload + async def start_activity( + self, + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[Any]: ... + + async def start_activity( + self, + activity: ( + str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] + ), + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + # Either schedule_to_close_timeout or start_to_close_timeout must be present + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: + """Start an activity and return its handle. + + .. warning:: + This API is experimental. + + Args: + activity: String name or callable activity function to execute. + arg: Single argument to the activity. + args: Multiple arguments to the activity. Cannot be set if arg is. + id: Unique identifier for the activity. Required. + task_queue: Task queue to send the activity to. + result_type: For string name activities, optional type to deserialize result into. + schedule_to_close_timeout: Total time allowed for the activity from schedule to completion. + schedule_to_start_timeout: Time allowed for the activity to sit in the task queue. + start_to_close_timeout: Time allowed for a single execution attempt. + heartbeat_timeout: Time between heartbeats before the activity is considered failed. + id_reuse_policy: How to handle reusing activity IDs from closed activities. + Default is ALLOW_DUPLICATE. + id_conflict_policy: How to handle activity ID conflicts with running activities. + Default is FAIL. + retry_policy: Retry policy for the activity. + search_attributes: Search attributes for the activity. + summary: A single-line fixed summary for this activity that may appear + in the UI/CLI. This can be in single-line Temporal markdown format. + priority: Priority of the activity execution. + start_delay: Time to wait before dispatching the activity. + This delay is not applied to retry attempts. + rpc_metadata: Headers used on the RPC call. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + A handle to the started activity. + """ + name, result_type_from_type_annotation = ( + temporalio.activity._Definition.get_name_and_result_type(activity) + ) + return await self._impl.start_activity( + StartActivityInput( + activity_type=name, + args=temporalio.common._arg_or_args(arg, args), + id=id, + task_queue=task_queue, + result_type=result_type or result_type_from_type_annotation, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + start_delay=start_delay, + headers={}, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + priority=priority, + ) + ) + + # async no-param + @overload + async def execute_activity( + self, + activity: CallableAsyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync no-param + @overload + async def execute_activity( + self, + activity: CallableSyncNoParam[ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async single-param + @overload + async def execute_activity( + self, + activity: CallableAsyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync single-param + @overload + async def execute_activity( + self, + activity: CallableSyncSingleParam[ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async multi-param + @overload + async def execute_activity( + self, + activity: Callable[..., Awaitable[ReturnType]], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync multi-param + @overload + async def execute_activity( + self, + activity: Callable[..., ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # string name + @overload + async def execute_activity( + self, + activity: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def execute_activity( + self, + activity: ( + str | Callable[..., Awaitable[ReturnType]] | Callable[..., ReturnType] + ), + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + # Either schedule_to_close_timeout or start_to_close_timeout must be present + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Start an activity, wait for it to complete, and return its result. + + .. warning:: + This API is experimental. + + This is a convenience method that combines :py:meth:`start_activity` and + :py:meth:`ActivityHandle.result`. + + Returns: + The result of the activity. + + Raises: + ActivityFailureError: If the activity completed with a failure. + """ + handle: ActivityHandle[ReturnType] = await self.start_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + # async no-param + @overload + async def start_activity_class( + self, + activity: type[CallableAsyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync no-param + @overload + async def start_activity_class( + self, + activity: type[CallableSyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async single-param + @overload + async def start_activity_class( + self, + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync single-param + @overload + async def start_activity_class( + self, + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async multi-param + @overload + async def start_activity_class( + self, + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync multi-param + @overload + async def start_activity_class( + self, + activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + async def start_activity_class( + self, + activity: type[Callable], # type: ignore[reportInvalidTypeForm] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[Any]: + """Start an activity from a callable class. + + .. warning:: + This API is experimental. + + See :py:meth:`start_activity` for parameter and return details. + """ + return await self.start_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + # async no-param + @overload + async def execute_activity_class( + self, + activity: type[CallableAsyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync no-param + @overload + async def execute_activity_class( + self, + activity: type[CallableSyncNoParam[ReturnType]], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async single-param + @overload + async def execute_activity_class( + self, + activity: type[CallableAsyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync single-param + @overload + async def execute_activity_class( + self, + activity: type[CallableSyncSingleParam[ParamType, ReturnType]], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async multi-param + @overload + async def execute_activity_class( + self, + activity: type[Callable[..., Awaitable[ReturnType]]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync multi-param + @overload + async def execute_activity_class( + self, + activity: type[Callable[..., ReturnType]], # type: ignore[reportInvalidTypeForm] + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + async def execute_activity_class( + self, + activity: type[Callable], # type: ignore[reportInvalidTypeForm] + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Start an activity from a callable class and wait for completion. + + .. warning:: + This API is experimental. + + This is a shortcut for ``await`` :py:meth:`start_activity_class`. + """ + return await self.execute_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + # async no-param + @overload + async def start_activity_method( + self, + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async single-param + @overload + async def start_activity_method( + self, + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # async multi-param + @overload + async def start_activity_method( + self, + activity: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + # sync multi-param + @overload + async def start_activity_method( + self, + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[ReturnType]: ... + + async def start_activity_method( + self, + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityHandle[Any]: + """Start an activity from a method. + + .. warning:: + This API is experimental. + + See :py:meth:`start_activity` for parameter and return details. + """ + return await self.start_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + # async no-param + @overload + async def execute_activity_method( + self, + activity: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async single-param + @overload + async def execute_activity_method( + self, + activity: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # async multi-param + @overload + async def execute_activity_method( + self, + activity: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + # sync multi-param + @overload + async def execute_activity_method( + self, + activity: Callable[Concatenate[SelfType, MultiParamSpec], ReturnType], + *, + args: Sequence[Any], + id: str, + task_queue: str, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: ... + + async def execute_activity_method( + self, + activity: Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + result_type: type | None = None, + schedule_to_close_timeout: timedelta | None = None, + schedule_to_start_timeout: timedelta | None = None, + start_to_close_timeout: timedelta | None = None, + heartbeat_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.ActivityIDReusePolicy = temporalio.common.ActivityIDReusePolicy.ALLOW_DUPLICATE, + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy = temporalio.common.ActivityIDConflictPolicy.FAIL, + retry_policy: temporalio.common.RetryPolicy | None = None, + search_attributes: temporalio.common.TypedSearchAttributes | None = None, + summary: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Start an activity from a method and wait for completion. + + .. warning:: + This API is experimental. + + This is a shortcut for ``await`` :py:meth:`start_activity_method`. + """ + return await self.execute_activity( + cast(Any, activity), + arg, + args=args, + id=id, + task_queue=task_queue, + result_type=result_type, + schedule_to_close_timeout=schedule_to_close_timeout, + schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + heartbeat_timeout=heartbeat_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + search_attributes=search_attributes, + summary=summary, + priority=priority, + start_delay=start_delay, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + def list_activities( + self, + query: str, + *, + limit: int | None = None, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityExecutionAsyncIterator: + """List activities not started by a workflow. + + .. warning:: + This API is experimental. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + query: A Temporal visibility list filter for activities. Required. + limit: Maximum number of activities to return. If unset, all + activities are returned. Only applies if using the + returned :py:class:`ActivityExecutionAsyncIterator` + as an async iterator. + page_size: Maximum number of results for each page. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_activities( + ListActivitiesInput( + query=query, + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + limit=limit, + ) + ) + + async def count_activities( + self, + query: str | None = None, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ActivityExecutionCount: + """Count activities not started by a workflow. + + .. warning:: + This API is experimental. + + Args: + query: A Temporal visibility filter for activities. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Count of activities. + """ + return await self._impl.count_activities( + CountActivitiesInput( + query=query, rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + ) + + @overload + def get_activity_handle( + self, + activity_id: str, + *, + run_id: str | None = None, + ) -> ActivityHandle[Any]: ... + + @overload + def get_activity_handle( + self, + activity_id: str, + *, + run_id: str | None = None, + result_type: type[ReturnType], + ) -> ActivityHandle[ReturnType]: ... + + def get_activity_handle( + self, + activity_id: str, + *, + run_id: str | None = None, + result_type: type | None = None, + ) -> ActivityHandle[Any]: + """Get a handle to an existing activity, as the caller of that activity. + + The activity must not have been started by a workflow. + + .. warning:: + This API is experimental. + + To get a handle to an activity execution that you control for manual completion and + heartbeating, see :py:meth:`Client.get_async_activity_handle`. + + Args: + activity_id: The activity ID. + run_id: The activity run ID. If not provided, targets the latest run. + result_type: The result type to deserialize into. + + Returns: + A handle to the activity. + """ + return ActivityHandle( + self, + activity_id, + run_id=run_id, + result_type=result_type, + ) + + @overload + def get_async_activity_handle( + self, *, activity_id: str, run_id: str | None = None + ) -> AsyncActivityHandle: + pass + + @overload + def get_async_activity_handle( + self, *, workflow_id: str, run_id: str | None, activity_id: str + ) -> AsyncActivityHandle: + pass + + @overload + def get_async_activity_handle(self, *, task_token: bytes) -> AsyncActivityHandle: + pass + + def get_async_activity_handle( + self, + *, + workflow_id: str | None = None, + run_id: str | None = None, + activity_id: str | None = None, + task_token: bytes | None = None, + ) -> AsyncActivityHandle: + """Get a handle to an activity execution that you control, for manual + completion and heartbeating. + + To get a handle to an activity execution as the caller of that activity, + see :py:meth:`Client.get_activity_handle`. + + This function may be used to get a handle to an activity started by a + client, or an activity started by a workflow. + + To get a handle to an activity started by a workflow, use one of the + following two calls: + - Supply ``workflow_id``, ``run_id``, and ``activity_id`` + - Supply the activity ``task_token`` alone + + To get a handle to an activity not started by a workflow, supply + ``activity_id`` and ``run_id`` + + Args: + workflow_id: Workflow ID for the activity, or None if not a workflow + activity. Cannot be set if task_token is set. + run_id: Run ID for the activity or workflow. Cannot be set if + task_token is set. + activity_id: ID for the activity. Cannot be set if task_token is + set. + task_token: Task token for the activity. Cannot be set with other + fields. + + Returns: + A handle that can be used for completion or heartbeating. + """ + if task_token is not None: + if workflow_id is not None or run_id is not None or activity_id is not None: + raise ValueError("Task token cannot be present with other IDs") + return AsyncActivityHandle(self, task_token) + elif workflow_id is not None: + if activity_id is None: + raise ValueError( + "Workflow ID, run ID, and activity ID must all be given together" + ) + return AsyncActivityHandle( + self, + AsyncActivityIDReference( + workflow_id=workflow_id, run_id=run_id, activity_id=activity_id + ), + ) + elif activity_id is not None: + return AsyncActivityHandle( + self, + AsyncActivityIDReference( + activity_id=activity_id, + run_id=run_id, + workflow_id=None, + ), + ) + raise ValueError( + "Require task token, or workflow_id & run_id & activity_id, or activity_id & run_id" + ) + + async def create_schedule( + self, + id: str, + schedule: Schedule, + *, + trigger_immediately: bool = False, + backfill: Sequence[ScheduleBackfill] = [], + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ScheduleHandle: + """Create a schedule and return its handle. + + Args: + id: Unique identifier of the schedule. + schedule: Schedule to create. + trigger_immediately: If true, trigger one action immediately when + creating the schedule. + backfill: Set of time periods to take actions on as if that time + passed right now. + memo: Memo for the schedule. Memo for a scheduled workflow is part + of the schedule action. + search_attributes: Search attributes for the schedule. Search + attributes for a scheduled workflow are part of the scheduled + action. The dictionary form of this is DEPRECATED, use + :py:class:`temporalio.common.TypedSearchAttributes`. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + A handle to the created schedule. + + Raises: + ScheduleAlreadyRunningError: If a schedule with this ID is already + running. + """ + temporalio.common._warn_on_deprecated_search_attributes(search_attributes) + return await self._impl.create_schedule( + CreateScheduleInput( + id=id, + schedule=schedule, + trigger_immediately=trigger_immediately, + backfill=backfill, + memo=memo, + search_attributes=search_attributes, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + def get_schedule_handle(self, id: str) -> ScheduleHandle: + """Get a schedule handle for the given ID.""" + return ScheduleHandle(self, id) + + async def list_schedules( + self, + query: str | None = None, + *, + page_size: int = 1000, + next_page_token: bytes | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ScheduleAsyncIterator: + """List schedules. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Note, this list is eventually consistent. Therefore if a schedule is + added or deleted, it may not be available in the list immediately. + + Args: + page_size: Maximum number of results for each page. + query: A Temporal visibility list filter. See Temporal documentation + concerning visibility list filters including behavior when left + unset. + next_page_token: A previously obtained next page token if doing + pagination. Usually not needed as the iterator automatically + starts from the beginning. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that can be used with ``async for``. + """ + return self._impl.list_schedules( + ListSchedulesInput( + page_size=page_size, + next_page_token=next_page_token, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + query=query, + ) + ) + + async def update_worker_build_id_compatibility( + self, + task_queue: str, + operation: BuildIdOp, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Used to add new Build IDs or otherwise update the relative compatibility of Build Ids as + defined on a specific task queue for the Worker Versioning feature. + + For more on this feature, see https://docs.temporal.io/workers#worker-versioning + + .. deprecated:: + Legacy API, see the docs above for new usage + + Args: + task_queue: The task queue to target. + operation: The operation to perform. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + """ + return await self._impl.update_worker_build_id_compatibility( + UpdateWorkerBuildIdCompatibilityInput( + task_queue, + operation, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def get_worker_build_id_compatibility( + self, + task_queue: str, + max_sets: int | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkerBuildIdVersionSets: + """Get the Build ID compatibility sets for a specific task queue. + + For more on this feature, see https://docs.temporal.io/workers#worker-versioning + + .. deprecated:: + Legacy API, see the docs above for new usage + + Args: + task_queue: The task queue to target. + max_sets: The maximum number of sets to return. If not specified, all sets will be + returned. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + """ + return await self._impl.get_worker_build_id_compatibility( + GetWorkerBuildIdCompatibilityInput( + task_queue, + max_sets, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def get_worker_task_reachability( + self, + build_ids: Sequence[str], + task_queues: Sequence[str] = [], + reachability_type: TaskReachabilityType | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkerTaskReachability: + """Determine if some Build IDs for certain Task Queues could have tasks dispatched to them. + + For more on this feature, see https://docs.temporal.io/workers#worker-versioning + + .. deprecated:: + Legacy API, see the docs above for new usage + + Args: + build_ids: The Build IDs to query the reachability of. At least one must be specified. + task_queues: Task Queues to restrict the query to. If not specified, all Task Queues + will be searched. When requesting a large number of task queues or all task queues + associated with the given Build IDs in a namespace, all Task Queues will be listed + in the response but some of them may not contain reachability information due to a + server enforced limit. When reaching the limit, task queues that reachability + information could not be retrieved for will be marked with a ``NotFetched`` entry in + {@link BuildIdReachability.taskQueueReachability}. The caller may issue another call + to get the reachability for those task queues. + reachability_type: The kind of reachability this request is concerned with. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + """ + return await self._impl.get_worker_task_reachability( + GetWorkerTaskReachabilityInput( + build_ids, + task_queues, + reachability_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + +class ClientConnectConfig(TypedDict, total=False): + """TypedDict of keyword arguments for :py:meth:`Client.connect`.""" + + target_host: str + namespace: str + api_key: str | None + data_converter: temporalio.converter.DataConverter + plugins: Sequence[Plugin] + interceptors: Sequence[Interceptor] + default_workflow_query_reject_condition: ( + temporalio.common.QueryRejectCondition | None + ) + tls: bool | TLSConfig | None + retry_config: RetryConfig | None + keep_alive_config: KeepAliveConfig | None + rpc_metadata: Mapping[str, str | bytes] + identity: str | None + lazy: bool + runtime: temporalio.runtime.Runtime | None + http_connect_proxy_config: HttpConnectProxyConfig | None + dns_load_balancing_config: DnsLoadBalancingConfig | None + header_codec_behavior: HeaderCodecBehavior + + +class ClientConfig(TypedDict, total=False): + """TypedDict of config originally passed to :py:meth:`Client`.""" + + service_client: Required[temporalio.service.ServiceClient] + namespace: Required[str] + data_converter: Required[temporalio.converter.DataConverter] + plugins: Required[Sequence[Plugin]] + interceptors: Required[Sequence[Interceptor]] + default_workflow_query_reject_condition: Required[ + temporalio.common.QueryRejectCondition | None + ] + header_codec_behavior: Required[HeaderCodecBehavior] diff --git a/temporalio/client/_cloud.py b/temporalio/client/_cloud.py new file mode 100644 index 000000000..51966666d --- /dev/null +++ b/temporalio/client/_cloud.py @@ -0,0 +1,181 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from collections.abc import ( + Mapping, +) + +import temporalio.runtime +import temporalio.service +from temporalio.service import ( + DnsLoadBalancingConfig, + HttpConnectProxyConfig, + KeepAliveConfig, + RetryConfig, + TLSConfig, +) + + +class CloudOperationsClient: + """Client for accessing Temporal Cloud Operations API. + + .. warning:: + This client and the API are experimental + + Most users will use :py:meth:`connect` to create a client. The + :py:attr:`cloud_service` property provides access to a raw gRPC cloud + service client. + + Clients are not thread-safe and should only be used in the event loop they + are first connected in. If a client needs to be used from another thread + than where it was created, make sure the event loop where it was created is + captured, and then call :py:func:`asyncio.run_coroutine_threadsafe` with the + client call and that event loop. + + Clients do not work across forks since runtimes do not work across forks. + """ + + @staticmethod + async def connect( + *, + api_key: str | None = None, + version: str | None = None, + target_host: str = "saas-api.tmprl.cloud:443", + tls: bool | TLSConfig = True, + retry_config: RetryConfig | None = None, + keep_alive_config: KeepAliveConfig | None = KeepAliveConfig.default, + rpc_metadata: Mapping[str, str | bytes] = {}, + identity: str | None = None, + lazy: bool = False, + runtime: temporalio.runtime.Runtime | None = None, + http_connect_proxy_config: HttpConnectProxyConfig | None = None, + dns_load_balancing_config: DnsLoadBalancingConfig | None = None, + ) -> CloudOperationsClient: + """Connect to a Temporal Cloud Operations API. + + .. warning:: + This client and the API are experimental + + Args: + api_key: API key for Temporal. This becomes the "Authorization" + HTTP header with "Bearer " prepended. This is only set if RPC + metadata doesn't already have an "authorization" key. This is + essentially required for access to the cloud API. + version: Version header for safer mutations. May or may not be + required depending on cloud settings. + target_host: ``host:port`` for the Temporal server. The default is + to the common cloud endpoint. + tls: If true, the default, use system default TLS configuration. If + false, the default, do not use TLS. If TLS configuration + present, that TLS configuration will be used. The default is + usually required to access the API. + retry_config: Retry configuration for direct service calls (when + opted in) or all high-level calls made by this client (which all + opt-in to retries by default). If unset, a default retry + configuration is used. + keep_alive_config: Keep-alive configuration for the client + connection. Default is to check every 30s and kill the + connection if a response doesn't come back in 15s. Can be set to + ``None`` to disable. + rpc_metadata: Headers to use for all calls to the server. Keys here + can be overriden by per-call RPC metadata keys. + identity: Identity for this client. If unset, a default is created + based on the version of the SDK. + lazy: If true, the client will not connect until the first call is + attempted or a worker is created with it. Lazy clients cannot be + used for workers. + runtime: The runtime for this client, or the default if unset. + http_connect_proxy_config: Configuration for HTTP CONNECT proxy. + dns_load_balancing_config: DNS load balancing configuration for the + client connection. Default is disabled. Silently disabled when + ``http_connect_proxy_config`` is set, since the two are mutually + exclusive. + """ + # Add version if given + if version: + rpc_metadata = dict(rpc_metadata) + rpc_metadata["temporal-cloud-api-version"] = version + connect_config = temporalio.service.ConnectConfig( + target_host=target_host, + api_key=api_key, + tls=tls, + retry_config=retry_config, + keep_alive_config=keep_alive_config, + rpc_metadata=rpc_metadata, + identity=identity or "", + lazy=lazy, + runtime=runtime, + http_connect_proxy_config=http_connect_proxy_config, + dns_load_balancing_config=dns_load_balancing_config, + ) + return CloudOperationsClient( + await temporalio.service.ServiceClient.connect(connect_config) + ) + + def __init__( + self, + service_client: temporalio.service.ServiceClient, + ): + """Create a Temporal Cloud Operations client from a service client. + + .. warning:: + This client and the API are experimental + + Args: + service_client: Existing service client to use. + """ + self._service_client = service_client + + @property + def service_client(self) -> temporalio.service.ServiceClient: + """Raw gRPC service client.""" + return self._service_client + + @property + def cloud_service(self) -> temporalio.service.CloudService: + """Raw gRPC cloud service client.""" + return self._service_client.cloud_service + + @property + def identity(self) -> str: + """Identity used in calls by this client.""" + return self._service_client.config.identity + + @property + def rpc_metadata(self) -> Mapping[str, str | bytes]: + """Headers for every call made by this client. + + Do not use mutate this mapping. Rather, set this property with an + entirely new mapping to change the headers. This may include the + ``temporal-cloud-api-version`` header if set. + """ + return self.service_client.config.rpc_metadata + + @rpc_metadata.setter + def rpc_metadata(self, value: Mapping[str, str | bytes]) -> None: + """Update the headers for this client. + + Do not mutate this mapping after set. Rather, set an entirely new + mapping if changes are needed. Currently this must be set with the + ``temporal-cloud-api-version`` header if it is needed. + """ + # Update config and perform update + self.service_client.config.rpc_metadata = value + self.service_client.update_rpc_metadata(value) + + @property + def api_key(self) -> str | None: + """API key for every call made by this client.""" + return self.service_client.config.api_key + + @api_key.setter + def api_key(self, value: str | None) -> None: + """Update the API key for this client. + + This is only set if RPCmetadata doesn't already have an "authorization" + key. + """ + # Update config and perform update + self.service_client.config.api_key = value + self.service_client.update_api_key(value) diff --git a/temporalio/client/_exceptions.py b/temporalio/client/_exceptions.py new file mode 100644 index 000000000..fdcd263e6 --- /dev/null +++ b/temporalio/client/_exceptions.py @@ -0,0 +1,139 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, +) + +import temporalio.exceptions +from temporalio.activity import ActivityCancellationDetails + +if TYPE_CHECKING: + from ._workflow import WorkflowExecutionStatus + + +class WorkflowFailureError(temporalio.exceptions.TemporalError): + """Error that occurs when a workflow is unsuccessful.""" + + def __init__(self, *, cause: BaseException) -> None: + """Create workflow failure error.""" + super().__init__("Workflow execution failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the workflow failure.""" + assert self.__cause__ + return self.__cause__ + + +class WorkflowContinuedAsNewError(temporalio.exceptions.TemporalError): + """Error that occurs when a workflow was continued as new.""" + + def __init__(self, new_execution_run_id: str) -> None: + """Create workflow continue as new error.""" + super().__init__("Workflow continued as new") + self._new_execution_run_id = new_execution_run_id + + @property + def new_execution_run_id(self) -> str: + """New execution run ID the workflow continued to""" + return self._new_execution_run_id + + +class WorkflowQueryRejectedError(temporalio.exceptions.TemporalError): + """Error that occurs when a query was rejected.""" + + def __init__(self, status: WorkflowExecutionStatus | None) -> None: + """Create workflow query rejected error.""" + super().__init__(f"Query rejected, status: {status}") + self._status = status + + @property + def status(self) -> WorkflowExecutionStatus | None: + """Get workflow execution status causing rejection.""" + return self._status + + +class WorkflowQueryFailedError(temporalio.exceptions.TemporalError): + """Error that occurs when a query fails.""" + + def __init__(self, message: str) -> None: + """Create workflow query failed error.""" + super().__init__(message) + self._message = message + + @property + def message(self) -> str: + """Get query failed message.""" + return self._message + + +class WorkflowUpdateFailedError(temporalio.exceptions.TemporalError): + """Error that occurs when an update fails.""" + + def __init__(self, cause: BaseException) -> None: + """Create workflow update failed error.""" + super().__init__("Workflow update failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the update failure.""" + assert self.__cause__ + return self.__cause__ + + +class RPCTimeoutOrCancelledError(temporalio.exceptions.TemporalError): + """Error that occurs on some client calls that timeout or get cancelled.""" + + pass + + +class WorkflowUpdateRPCTimeoutOrCancelledError(RPCTimeoutOrCancelledError): + """Error that occurs when update RPC call times out or is cancelled. + + Note, this is not related to any general concept of timing out or cancelling + a running update, this is only related to the client call itself. + """ + + def __init__(self) -> None: + """Create workflow update timeout or cancelled error.""" + super().__init__("Timeout or cancellation waiting for update") + + +class ActivityFailureError(temporalio.exceptions.TemporalError): + """Error that occurs when an activity is unsuccessful. + + .. warning:: + This API is experimental. + """ + + def __init__(self, *, cause: BaseException) -> None: + """Create activity failure error.""" + super().__init__("Activity execution failed") + self.__cause__ = cause + + @property + def cause(self) -> BaseException: + """Cause of the activity failure.""" + assert self.__cause__ + return self.__cause__ + + +class AsyncActivityCancelledError(temporalio.exceptions.TemporalError): + """Error that occurs when async activity attempted heartbeat but was cancelled.""" + + def __init__(self, details: ActivityCancellationDetails | None = None) -> None: + """Create async activity cancelled error.""" + super().__init__("Activity cancelled") + self.details = details + + +class ScheduleAlreadyRunningError(temporalio.exceptions.TemporalError): + """Error when a schedule is already running.""" + + def __init__(self) -> None: + """Create schedule already running error.""" + super().__init__("Schedule already running") diff --git a/temporalio/client/_helpers.py b/temporalio/client/_helpers.py new file mode 100644 index 000000000..08a584651 --- /dev/null +++ b/temporalio/client/_helpers.py @@ -0,0 +1,202 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import copy +import json +import re +from collections.abc import ( + Iterable, + Mapping, +) +from typing import ( + Any, +) + +import google.protobuf.json_format +from google.protobuf.internal.containers import MessageMap + +import temporalio.api.common.v1 +import temporalio.api.history.v1 +import temporalio.api.sdk.v1 +import temporalio.common +import temporalio.converter +from temporalio.converter import ( + DataConverter, +) + + +async def _apply_headers( # pyright: ignore[reportUnusedFunction] + source: Mapping[str, temporalio.api.common.v1.Payload] | None, + dest: MessageMap[str, temporalio.api.common.v1.Payload], + encode_headers: bool, + data_converter: DataConverter, +) -> None: + if source is None: + return + if encode_headers: + for payload in source.values(): + payload.CopyFrom(await data_converter._transform_outbound_payload(payload)) + temporalio.common._apply_headers(source, dest) + + +def _history_from_json( # pyright: ignore[reportUnusedFunction] + history: str | dict[str, Any], +) -> temporalio.api.history.v1.History: + if isinstance(history, str): + history = json.loads(history) + else: + # Copy the dict so we can mutate it + history = copy.deepcopy(history) + if not isinstance(history, dict): + raise ValueError("JSON history not a dictionary") + events = history.get("events") + if not isinstance(events, Iterable): + raise ValueError("History does not have iterable 'events'") + for event in events: + if not isinstance(event, dict): + raise ValueError("Event not a dictionary") + _fix_history_enum( + "CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", + event, + "requestCancelExternalWorkflowExecutionFailedEventAttributes", + "cause", + ) + _fix_history_enum("CONTINUE_AS_NEW_INITIATOR", event, "*", "initiator") + _fix_history_enum("EVENT_TYPE", event, "eventType") + _fix_history_enum( + "PARENT_CLOSE_POLICY", + event, + "startChildWorkflowExecutionInitiatedEventAttributes", + "parentClosePolicy", + ) + _fix_history_enum("RETRY_STATE", event, "*", "retryState") + _fix_history_enum( + "SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE", + event, + "signalExternalWorkflowExecutionFailedEventAttributes", + "cause", + ) + _fix_history_enum( + "START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE", + event, + "startChildWorkflowExecutionFailedEventAttributes", + "cause", + ) + _fix_history_enum("TASK_QUEUE_KIND", event, "*", "taskQueue", "kind") + _fix_history_enum( + "TIMEOUT_TYPE", + event, + "workflowTaskTimedOutEventAttributes", + "timeoutType", + ) + _fix_history_enum( + "WORKFLOW_ID_REUSE_POLICY", + event, + "startChildWorkflowExecutionInitiatedEventAttributes", + "workflowIdReusePolicy", + ) + _fix_history_enum( + "WORKFLOW_TASK_FAILED_CAUSE", + event, + "workflowTaskFailedEventAttributes", + "cause", + ) + _fix_history_failure(event, "*", "failure") + _fix_history_failure(event, "activityTaskStartedEventAttributes", "lastFailure") + _fix_history_failure( + event, "workflowExecutionStartedEventAttributes", "continuedFailure" + ) + return google.protobuf.json_format.ParseDict( + history, temporalio.api.history.v1.History(), ignore_unknown_fields=True + ) + + +def _fix_history_failure(parent: dict[str, Any], *attrs: str) -> None: + _fix_history_enum( + "TIMEOUT_TYPE", parent, *attrs, "timeoutFailureInfo", "timeoutType" + ) + _fix_history_enum("RETRY_STATE", parent, *attrs, "*", "retryState") + # Recurse into causes. First collect all failure parents. + parents = [parent] + for attr in attrs: + new_parents = [] + for parent in parents: + if attr == "*": + for v in parent.values(): + if isinstance(v, dict): + new_parents.append(v) + else: + child = parent.get(attr) + if isinstance(child, dict): + new_parents.append(child) + if not new_parents: + return + parents = new_parents + # Fix each + for parent in parents: + _fix_history_failure(parent, "cause") + + +_pascal_case_match = re.compile("([A-Z]+)") + + +def _fix_history_enum(prefix: str, parent: dict[str, Any], *attrs: str) -> None: + # If the attr is "*", we need to handle all dict children + if attrs[0] == "*": + for child in parent.values(): + if isinstance(child, dict): + _fix_history_enum(prefix, child, *attrs[1:]) + else: + child = parent.get(attrs[0]) + if isinstance(child, str) and len(attrs) == 1: + # We only fix it if it doesn't already have the prefix + if not parent[attrs[0]].startswith(prefix): + parent[attrs[0]] = ( + prefix + _pascal_case_match.sub(r"_\1", child).upper() + ) + elif isinstance(child, dict) and len(attrs) > 1: + _fix_history_enum(prefix, child, *attrs[1:]) + elif isinstance(child, list) and len(attrs) > 1: + for child_item in child: + if isinstance(child_item, dict): + _fix_history_enum(prefix, child_item, *attrs[1:]) + + +async def _encode_user_metadata( # pyright: ignore[reportUnusedFunction] + converter: temporalio.converter.DataConverter, + summary: str | temporalio.api.common.v1.Payload | None, + details: str | temporalio.api.common.v1.Payload | None, +) -> temporalio.api.sdk.v1.UserMetadata | None: + if summary is None and details is None: + return None + enc_summary = None + enc_details = None + if summary is not None: + if isinstance(summary, str): + enc_summary = (await converter.encode([summary]))[0] + else: + enc_summary = summary + if details is not None: + if isinstance(details, str): + enc_details = (await converter.encode([details]))[0] + else: + enc_details = details + return temporalio.api.sdk.v1.UserMetadata(summary=enc_summary, details=enc_details) + + +async def _decode_user_metadata( # pyright: ignore[reportUnusedFunction] + converter: temporalio.converter.DataConverter, + metadata: temporalio.api.sdk.v1.UserMetadata | None, +) -> tuple[str | None, str | None]: + """Returns (summary, details)""" + if metadata is None: + return None, None + return ( + None + if not metadata.HasField("summary") + else (await converter.decode([metadata.summary]))[0], + None + if not metadata.HasField("details") + else (await converter.decode([metadata.details]))[0], + ) diff --git a/temporalio/client/_impl.py b/temporalio/client/_impl.py new file mode 100644 index 000000000..0c05a2dd7 --- /dev/null +++ b/temporalio/client/_impl.py @@ -0,0 +1,1410 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import asyncio +import inspect +import uuid +import warnings +from collections.abc import ( + Callable, + Mapping, +) +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, + cast, +) + +from google.protobuf.internal.containers import MessageMap + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.errordetails.v1 +import temporalio.api.failure.v1 +import temporalio.api.schedule.v1 +import temporalio.api.taskqueue.v1 +import temporalio.api.update.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.exceptions +import temporalio.nexus +import temporalio.nexus._operation_context +from temporalio.activity import ActivityCancellationDetails +from temporalio.converter import ( + ActivitySerializationContext, + StorageDriverActivityInfo, + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + WorkflowSerializationContext, +) +from temporalio.service import ( + RPCError, + RPCStatusCode, +) + +from ..common import HeaderCodecBehavior +from ._activity import ( + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityExecutionDescription, + ActivityHandle, + AsyncActivityIDReference, +) +from ._exceptions import ( + AsyncActivityCancelledError, + ScheduleAlreadyRunningError, + WorkflowQueryFailedError, + WorkflowQueryRejectedError, + WorkflowUpdateRPCTimeoutOrCancelledError, +) +from ._helpers import _apply_headers, _encode_user_metadata +from ._interceptor import ( + BackfillScheduleInput, + CancelActivityInput, + CancelWorkflowInput, + CompleteAsyncActivityInput, + CountActivitiesInput, + CountWorkflowsInput, + CreateScheduleInput, + DeleteScheduleInput, + DescribeActivityInput, + DescribeScheduleInput, + DescribeWorkflowInput, + FailAsyncActivityInput, + FetchWorkflowHistoryEventsInput, + GetWorkerBuildIdCompatibilityInput, + GetWorkerTaskReachabilityInput, + HeartbeatAsyncActivityInput, + ListActivitiesInput, + ListSchedulesInput, + ListWorkflowsInput, + OutboundInterceptor, + PauseScheduleInput, + QueryWorkflowInput, + ReportCancellationAsyncActivityInput, + SignalWorkflowInput, + StartActivityInput, + StartWorkflowInput, + StartWorkflowUpdateInput, + StartWorkflowUpdateWithStartInput, + TerminateActivityInput, + TerminateWorkflowInput, + TriggerScheduleInput, + UnpauseScheduleInput, + UpdateScheduleInput, + UpdateWithStartStartWorkflowInput, + UpdateWithStartUpdateWorkflowInput, + UpdateWorkerBuildIdCompatibilityInput, +) +from ._schedule import ( + ScheduleAsyncIterator, + ScheduleDescription, + ScheduleHandle, + ScheduleUpdate, + ScheduleUpdateInput, +) +from ._worker_versioning import WorkerBuildIdVersionSets, WorkerTaskReachability +from ._workflow import ( + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowExecutionDescription, + WorkflowExecutionStatus, + WorkflowHandle, + WorkflowHistoryEventAsyncIterator, + WorkflowUpdateHandle, + WorkflowUpdateStage, +) + +if TYPE_CHECKING: + from ._client import Client + + +class _ClientImpl(OutboundInterceptor): # pyright: ignore[reportUnusedClass] + def __init__(self, client: Client) -> None: # type: ignore + # We are intentionally not calling the base class's __init__ here + self._client = client + + ### Workflow calls + + async def start_workflow( + self, input: StartWorkflowInput + ) -> WorkflowHandle[Any, Any]: + req: ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + ) + if input.start_signal is not None: + req = await self._build_signal_with_start_workflow_execution_request(input) + else: + req = await self._build_start_workflow_execution_request(input) + + resp: ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + ) + first_execution_run_id = None + eagerly_started = False + try: + if isinstance( + req, + temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest, + ): + resp = await self._client.workflow_service.signal_with_start_workflow_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + resp = await self._client.workflow_service.start_workflow_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + first_execution_run_id = resp.run_id + eagerly_started = resp.HasField("eager_workflow_task") + except RPCError as err: + # If the status is ALREADY_EXISTS and the details can be extracted + # as already started, use a different exception + if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: + details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() + if err.grpc_status.details[0].Unpack(details): + raise temporalio.exceptions.WorkflowAlreadyStartedError( + input.id, input.workflow, run_id=details.run_id + ) + raise + handle: WorkflowHandle[Any, Any] = WorkflowHandle( + self._client, + req.workflow_id, + result_run_id=resp.run_id, + first_execution_run_id=first_execution_run_id, + result_type=input.ret_type, + start_workflow_response=resp, + ) + setattr(handle, "__temporal_eagerly_started", eagerly_started) + return handle + + async def _build_start_workflow_execution_request( + self, input: StartWorkflowInput + ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: + req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() + await self._populate_start_workflow_execution_request(req, input) + # _populate_start_workflow_execution_request is used for both StartWorkflowInput + # and UpdateWithStartStartWorkflowInput. UpdateWithStartStartWorkflowInput does + # not have the following two fields so they are handled here. + req.request_eager_execution = input.request_eager_start + if input.request_id: + req.request_id = input.request_id + + links = [ + temporalio.api.common.v1.Link(workflow_event=link) + for link in input.workflow_event_links + ] + req.completion_callbacks.extend( + temporalio.api.common.v1.Callback( + nexus=temporalio.api.common.v1.Callback.Nexus( + url=callback.url, + header=callback.headers, + ), + links=links, + ) + for callback in input.callbacks + ) + # Links are duplicated on request for compatibility with older server versions. + req.links.extend(links) + + if temporalio.nexus._operation_context._in_nexus_backing_workflow_start_context(): + req.on_conflict_options.attach_request_id = True + req.on_conflict_options.attach_completion_callbacks = True + req.on_conflict_options.attach_links = True + + return req + + async def _build_signal_with_start_workflow_execution_request( + self, input: StartWorkflowInput + ) -> temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest: + assert input.start_signal + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, type=input.workflow, namespace=self._client.namespace + ), + ), + ) + req = temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest( + signal_name=input.start_signal + ) + if input.start_signal_args: + req.signal_input.payloads.extend( + await data_converter.encode(input.start_signal_args) + ) + await self._populate_start_workflow_execution_request(req, input) + return req + + async def _build_update_with_start_start_workflow_execution_request( + self, input: UpdateWithStartStartWorkflowInput + ) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: + req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() + await self._populate_start_workflow_execution_request(req, input) + return req + + async def _populate_start_workflow_execution_request( + self, + req: ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest + ), + input: StartWorkflowInput | UpdateWithStartStartWorkflowInput, + ) -> None: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, type=input.workflow, namespace=self._client.namespace + ), + ), + ) + req.namespace = self._client.namespace + req.workflow_id = input.id + req.workflow_type.name = input.workflow + req.task_queue.name = input.task_queue + if input.args: + req.input.payloads.extend(await data_converter.encode(input.args)) + if input.execution_timeout is not None: + req.workflow_execution_timeout.FromTimedelta(input.execution_timeout) + if input.run_timeout is not None: + req.workflow_run_timeout.FromTimedelta(input.run_timeout) + if input.task_timeout is not None: + req.workflow_task_timeout.FromTimedelta(input.task_timeout) + req.identity = self._client.identity + req.request_id = str(uuid.uuid4()) + req.workflow_id_reuse_policy = cast( + "temporalio.api.enums.v1.WorkflowIdReusePolicy.ValueType", + int(input.id_reuse_policy), + ) + req.workflow_id_conflict_policy = cast( + "temporalio.api.enums.v1.WorkflowIdConflictPolicy.ValueType", + int(input.id_conflict_policy), + ) + + if input.retry_policy is not None: + input.retry_policy.apply_to_proto(req.retry_policy) + req.cron_schedule = input.cron_schedule + if input.memo is not None: + await data_converter._encode_memo_existing(input.memo, req.memo) + if input.search_attributes is not None: + temporalio.converter.encode_search_attributes( + input.search_attributes, req.search_attributes + ) + metadata = await _encode_user_metadata( + data_converter, input.static_summary, input.static_details + ) + if metadata is not None: + req.user_metadata.CopyFrom(metadata) + if input.start_delay is not None: + req.workflow_start_delay.FromTimedelta(input.start_delay) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.header.fields) + if input.priority is not None: # type:ignore[reportUnnecessaryComparison] + req.priority.CopyFrom(input.priority._to_proto()) + if input.versioning_override is not None: + req.versioning_override.CopyFrom(input.versioning_override._to_proto()) + + async def cancel_workflow(self, input: CancelWorkflowInput) -> None: + await self._client.workflow_service.request_cancel_workflow_execution( + temporalio.api.workflowservice.v1.RequestCancelWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + first_execution_run_id=input.first_execution_run_id or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def describe_workflow( + self, input: DescribeWorkflowInput + ) -> WorkflowExecutionDescription: + return await WorkflowExecutionDescription._from_raw_description( + await self._client.workflow_service.describe_workflow_execution( + temporalio.api.workflowservice.v1.DescribeWorkflowExecutionRequest( + namespace=self._client.namespace, + execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ), + namespace=self._client.namespace, + converter=self._client.data_converter.with_context( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ) + ), + ) + + def fetch_workflow_history_events( + self, input: FetchWorkflowHistoryEventsInput + ) -> WorkflowHistoryEventAsyncIterator: + return WorkflowHistoryEventAsyncIterator(self._client, input) + + def list_workflows( + self, input: ListWorkflowsInput + ) -> WorkflowExecutionAsyncIterator: + return WorkflowExecutionAsyncIterator(self._client, input) + + async def count_workflows( + self, input: CountWorkflowsInput + ) -> WorkflowExecutionCount: + return WorkflowExecutionCount._from_raw( + await self._client.workflow_service.count_workflow_executions( + temporalio.api.workflowservice.v1.CountWorkflowExecutionsRequest( + namespace=self._client.namespace, + query=input.query or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + ) + + async def query_workflow(self, input: QueryWorkflowInput) -> Any: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, + run_id=input.run_id or None, + namespace=self._client.namespace, + ), + ), + ) + req = temporalio.api.workflowservice.v1.QueryWorkflowRequest( + namespace=self._client.namespace, + execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + ) + if input.reject_condition: + req.query_reject_condition = cast( + "temporalio.api.enums.v1.QueryRejectCondition.ValueType", + int(input.reject_condition), + ) + req.query.query_type = input.query + if input.args: + req.query.query_args.payloads.extend( + await data_converter.encode(input.args) + ) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.query.header.fields) + try: + resp = await self._client.workflow_service.query_workflow( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + # If the status is INVALID_ARGUMENT, we can assume it's a query + # failed error + if err.status == RPCStatusCode.INVALID_ARGUMENT: + raise WorkflowQueryFailedError(err.message) + else: + raise + if resp.HasField("query_rejected"): + raise WorkflowQueryRejectedError( + WorkflowExecutionStatus(resp.query_rejected.status) + if resp.query_rejected.status + else None + ) + if not resp.query_result.payloads: + return None + type_hints = [input.ret_type] if input.ret_type else None + results = await data_converter.decode(resp.query_result.payloads, type_hints) + if not results: + return None + elif len(results) > 1: + warnings.warn(f"Expected single query result, got {len(results)}") + return results[0] + + async def signal_workflow(self, input: SignalWorkflowInput) -> None: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, + run_id=input.run_id or None, + namespace=self._client.namespace, + ), + ), + ) + req = temporalio.api.workflowservice.v1.SignalWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + signal_name=input.signal, + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ) + if input.args: + req.input.payloads.extend(await data_converter.encode(input.args)) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.header.fields) + await self._client.workflow_service.signal_workflow_execution( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + + async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=input.id, + run_id=input.run_id or None, + namespace=self._client.namespace, + ), + ), + ) + req = temporalio.api.workflowservice.v1.TerminateWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=input.id, + run_id=input.run_id or "", + ), + reason=input.reason or "", + identity=self._client.identity, + first_execution_run_id=input.first_execution_run_id or "", + ) + if input.args: + req.details.payloads.extend(await data_converter.encode(input.args)) + await self._client.workflow_service.terminate_workflow_execution( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + + async def start_activity(self, input: StartActivityInput) -> ActivityHandle[Any]: + """Start an activity and return a handle to it.""" + if not (input.start_to_close_timeout or input.schedule_to_close_timeout): + raise ValueError( + "Activity must have start_to_close_timeout or schedule_to_close_timeout" + ) + if input.start_delay is not None and input.start_delay < timedelta(0): + raise ValueError("start_delay must be non-negative") + req = await self._build_start_activity_execution_request(input) + + resp: temporalio.api.workflowservice.v1.StartActivityExecutionResponse + try: + resp = await self._client.workflow_service.start_activity_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + # If the status is ALREADY_EXISTS and the details can be extracted + # as already started, use a different exception + if err.status == RPCStatusCode.ALREADY_EXISTS and err.grpc_status.details: + details = temporalio.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure() + if err.grpc_status.details[0].Unpack(details): + raise temporalio.exceptions.ActivityAlreadyStartedError( + input.id, input.activity_type, run_id=details.run_id + ) + raise + return ActivityHandle( + self._client, + input.id, + run_id=resp.run_id, + result_type=input.result_type, + ) + + async def _build_start_activity_execution_request( + self, input: StartActivityInput + ) -> temporalio.api.workflowservice.v1.StartActivityExecutionRequest: + """Build StartActivityExecutionRequest from input.""" + data_converter = self._client.data_converter._with_contexts( + ActivitySerializationContext( + namespace=self._client.namespace, + activity_id=input.id, + activity_type=input.activity_type, + activity_task_queue=input.task_queue, + is_local=False, + workflow_id=None, + workflow_type=None, + ), + StorageDriverStoreContext( + target=StorageDriverActivityInfo( + id=input.id, + type=input.activity_type, + namespace=self._client.namespace, + ), + ), + ) + + req = temporalio.api.workflowservice.v1.StartActivityExecutionRequest( + namespace=self._client.namespace, + identity=self._client.identity, + activity_id=input.id, + activity_type=temporalio.api.common.v1.ActivityType( + name=input.activity_type + ), + task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=input.task_queue), + id_reuse_policy=cast( + "temporalio.api.enums.v1.ActivityIdReusePolicy.ValueType", + int(input.id_reuse_policy), + ), + id_conflict_policy=cast( + "temporalio.api.enums.v1.ActivityIdConflictPolicy.ValueType", + int(input.id_conflict_policy), + ), + ) + + if input.schedule_to_close_timeout is not None: + req.schedule_to_close_timeout.FromTimedelta(input.schedule_to_close_timeout) + if input.start_to_close_timeout is not None: + req.start_to_close_timeout.FromTimedelta(input.start_to_close_timeout) + if input.schedule_to_start_timeout is not None: + req.schedule_to_start_timeout.FromTimedelta(input.schedule_to_start_timeout) + if input.heartbeat_timeout is not None: + req.heartbeat_timeout.FromTimedelta(input.heartbeat_timeout) + if input.start_delay is not None: + req.start_delay.FromTimedelta(input.start_delay) + if input.retry_policy is not None: + input.retry_policy.apply_to_proto(req.retry_policy) + + # Set input payloads + if input.args: + req.input.payloads.extend(await data_converter.encode(input.args)) + + # Set search attributes + if input.search_attributes is not None: + temporalio.converter.encode_search_attributes( + input.search_attributes, req.search_attributes + ) + + # Set user metadata + metadata = await _encode_user_metadata(data_converter, input.summary, None) + if metadata is not None: + req.user_metadata.CopyFrom(metadata) + + # Set headers + if input.headers: + await self._apply_headers(input.headers, req.header.fields) + + # Set priority + req.priority.CopyFrom(input.priority._to_proto()) + + return req + + async def cancel_activity(self, input: CancelActivityInput) -> None: + """Cancel an activity.""" + await self._client.workflow_service.request_cancel_activity_execution( + temporalio.api.workflowservice.v1.RequestCancelActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=input.activity_id, + run_id=input.activity_run_id or "", + identity=self._client.identity, + request_id=str(uuid.uuid4()), + reason=input.reason or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def terminate_activity(self, input: TerminateActivityInput) -> None: + """Terminate an activity.""" + await self._client.workflow_service.terminate_activity_execution( + temporalio.api.workflowservice.v1.TerminateActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=input.activity_id, + run_id=input.activity_run_id or "", + reason=input.reason or "", + identity=self._client.identity, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def describe_activity( + self, input: DescribeActivityInput + ) -> ActivityExecutionDescription: + """Describe an activity.""" + resp = await self._client.workflow_service.describe_activity_execution( + temporalio.api.workflowservice.v1.DescribeActivityExecutionRequest( + namespace=self._client.namespace, + activity_id=input.activity_id, + run_id=input.activity_run_id or "", + long_poll_token=input.long_poll_token or b"", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + return await ActivityExecutionDescription._from_execution_info( + info=resp.info, + long_poll_token=resp.long_poll_token or None, + namespace=self._client.namespace, + data_converter=self._client.data_converter.with_context( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=input.activity_id, # Using activity_id as workflow_id for activities not started by a workflow + ) + ), + ) + + def list_activities( + self, input: ListActivitiesInput + ) -> ActivityExecutionAsyncIterator: + return ActivityExecutionAsyncIterator(self._client, input) + + async def count_activities( + self, input: CountActivitiesInput + ) -> ActivityExecutionCount: + return ActivityExecutionCount._from_raw( + await self._client.workflow_service.count_activity_executions( + temporalio.api.workflowservice.v1.CountActivityExecutionsRequest( + namespace=self._client.namespace, + query=input.query or "", + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + ) + + async def start_workflow_update( + self, input: StartWorkflowUpdateInput + ) -> WorkflowUpdateHandle[Any]: + workflow_id = input.id + req = await self._build_update_workflow_execution_request(input, workflow_id) + + # Repeatedly try to invoke UpdateWorkflowExecution until the update is durable. + resp: temporalio.api.workflowservice.v1.UpdateWorkflowExecutionResponse + while True: + try: + resp = await self._client.workflow_service.update_workflow_execution( + req, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + if ( + err.status == RPCStatusCode.DEADLINE_EXCEEDED + or err.status == RPCStatusCode.CANCELLED + ): + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + else: + raise + except asyncio.CancelledError as err: + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + if ( + resp.stage + >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED + ): + break + + # Build the handle. If the user's wait stage is COMPLETED, make sure we + # poll for result. + handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( + client=self._client, + id=req.request.meta.update_id, + workflow_id=workflow_id, + workflow_run_id=resp.update_ref.workflow_execution.run_id, + result_type=input.ret_type, + ) + if resp.HasField("outcome"): + handle._known_outcome = resp.outcome + if input.wait_for_stage == WorkflowUpdateStage.COMPLETED: + await handle._poll_until_outcome() + return handle + + async def _build_update_workflow_execution_request( + self, + input: StartWorkflowUpdateInput | UpdateWithStartUpdateWorkflowInput, + workflow_id: str, + ) -> temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest: + data_converter = self._client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=workflow_id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=workflow_id, + run_id=(input.run_id or None) + if isinstance(input, StartWorkflowUpdateInput) + else None, + namespace=self._client.namespace, + ), + ), + ) + run_id, first_execution_run_id = ( + ( + input.run_id, + input.first_execution_run_id, + ) + if isinstance(input, StartWorkflowUpdateInput) + else (None, None) + ) + req = temporalio.api.workflowservice.v1.UpdateWorkflowExecutionRequest( + namespace=self._client.namespace, + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=workflow_id, + run_id=run_id or "", + ), + first_execution_run_id=first_execution_run_id or "", + request=temporalio.api.update.v1.Request( + meta=temporalio.api.update.v1.Meta( + update_id=input.update_id or str(uuid.uuid4()), + identity=self._client.identity, + ), + input=temporalio.api.update.v1.Input( + name=input.update, + ), + ), + wait_policy=temporalio.api.update.v1.WaitPolicy( + lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.ValueType( + input.wait_for_stage + ) + ), + ) + if input.args: + req.request.input.args.payloads.extend( + await data_converter.encode(input.args) + ) + if input.headers is not None: # type:ignore[reportUnnecessaryComparison] + await self._apply_headers(input.headers, req.request.input.header.fields) + return req + + async def start_update_with_start_workflow( + self, input: StartWorkflowUpdateWithStartInput + ) -> WorkflowUpdateHandle[Any]: + seen_start = False + + def on_start( + start_response: temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse, + ): + nonlocal seen_start + if not seen_start: + input._on_start(start_response) + seen_start = True + + err: BaseException | None = None + + try: + return await self._start_workflow_update_with_start( + input.start_workflow_input, input.update_workflow_input, on_start + ) + except asyncio.CancelledError as _err: + err = _err + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + except RPCError as _err: + err = _err + if err.status in [ + RPCStatusCode.DEADLINE_EXCEEDED, + RPCStatusCode.CANCELLED, + ]: + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + else: + multiop_failure = ( + temporalio.api.errordetails.v1.MultiOperationExecutionFailure() + ) + if err.grpc_status.details and err.grpc_status.details[0].Unpack( + multiop_failure + ): + status = next( + ( + st + for st in multiop_failure.statuses + if ( + st.code != RPCStatusCode.OK + and not ( + st.details + and st.details[0].Is( + temporalio.api.failure.v1.MultiOperationExecutionAborted.DESCRIPTOR + ) + ) + ) + ), + None, + ) + if status and status.code in list(RPCStatusCode): + if ( + status.code == RPCStatusCode.ALREADY_EXISTS + and status.details + ): + details = temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure() + if status.details[0].Unpack(details): + err = temporalio.exceptions.WorkflowAlreadyStartedError( + input.start_workflow_input.id, + input.start_workflow_input.workflow, + run_id=details.run_id, + ) + else: + err = RPCError( + status.message, + RPCStatusCode(status.code), + err.raw_grpc_status, + ) + raise err + finally: + if err and not seen_start: + input._on_start_error(err) + + async def _start_workflow_update_with_start( + self, + start_input: UpdateWithStartStartWorkflowInput, + update_input: UpdateWithStartUpdateWorkflowInput, + on_start: Callable[ + [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None + ], + ) -> WorkflowUpdateHandle[Any]: + start_req = ( + await self._build_update_with_start_start_workflow_execution_request( + start_input + ) + ) + update_req = await self._build_update_workflow_execution_request( + update_input, workflow_id=start_input.id + ) + multiop_req = temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest( + namespace=self._client.namespace, + operations=[ + temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( + start_workflow=start_req + ), + temporalio.api.workflowservice.v1.ExecuteMultiOperationRequest.Operation( + update_workflow=update_req + ), + ], + ) + + # Repeatedly try to invoke ExecuteMultiOperation until the update is durable + while True: + multiop_response = ( + await self._client.workflow_service.execute_multi_operation(multiop_req) + ) + start_response = multiop_response.responses[0].start_workflow + update_response = multiop_response.responses[1].update_workflow + on_start(start_response) + known_outcome = ( + update_response.outcome if update_response.HasField("outcome") else None + ) + if ( + update_response.stage + >= temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED + ): + break + + handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle( + client=self._client, + id=update_req.request.meta.update_id, + workflow_id=start_input.id, + workflow_run_id=start_response.run_id, + known_outcome=known_outcome, + result_type=update_input.ret_type, + ) + if update_input.wait_for_stage == WorkflowUpdateStage.COMPLETED: + await handle._poll_until_outcome() + + return handle + + ### Async activity calls + + def _get_async_activity_store_context( + self, id_or_token: AsyncActivityIDReference | bytes + ) -> StorageDriverStoreContext: + if isinstance(id_or_token, AsyncActivityIDReference): + if id_or_token.workflow_id: + return StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=id_or_token.workflow_id or None, + run_id=id_or_token.run_id or None, + namespace=self._client.namespace, + ), + ) + return StorageDriverStoreContext( + target=StorageDriverActivityInfo( + id=id_or_token.activity_id, + run_id=id_or_token.run_id or None, + namespace=self._client.namespace, + ), + ) + else: + return StorageDriverStoreContext(target=None) + + async def heartbeat_async_activity( + self, input: HeartbeatAsyncActivityInput + ) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + details = ( + None + if not input.details + else await data_converter.encode_wrapper(input.details) + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + resp_by_id = await self._client.workflow_service.record_activity_task_heartbeat_by_id( + temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + if ( + resp_by_id.cancel_requested + or resp_by_id.activity_paused + or resp_by_id.activity_reset + ): + raise AsyncActivityCancelledError( + details=ActivityCancellationDetails( + cancel_requested=resp_by_id.cancel_requested, + paused=resp_by_id.activity_paused, + reset=resp_by_id.activity_reset, + ) + ) + + else: + resp = await self._client.workflow_service.record_activity_task_heartbeat( + temporalio.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + if resp.cancel_requested or resp.activity_paused: + raise AsyncActivityCancelledError( + details=ActivityCancellationDetails( + cancel_requested=resp.cancel_requested, + paused=resp.activity_paused, + reset=resp.activity_reset, + ) + ) + + async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + result = ( + None + if input.result is temporalio.common._arg_unset + else await data_converter.encode_wrapper([input.result]) + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + await self._client.workflow_service.respond_activity_task_completed_by_id( + temporalio.api.workflowservice.v1.RespondActivityTaskCompletedByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + result=result, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + await self._client.workflow_service.respond_activity_task_completed( + temporalio.api.workflowservice.v1.RespondActivityTaskCompletedRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + result=result, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + + failure = temporalio.api.failure.v1.Failure() + await data_converter.encode_failure(input.error, failure) + last_heartbeat_details = ( + await data_converter.encode_wrapper(input.last_heartbeat_details) + if input.last_heartbeat_details + else None + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + await self._client.workflow_service.respond_activity_task_failed_by_id( + temporalio.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + failure=failure, + last_heartbeat_details=last_heartbeat_details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + await self._client.workflow_service.respond_activity_task_failed( + temporalio.api.workflowservice.v1.RespondActivityTaskFailedRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + failure=failure, + last_heartbeat_details=last_heartbeat_details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def report_cancellation_async_activity( + self, input: ReportCancellationAsyncActivityInput + ) -> None: + data_converter = ( + input.data_converter_override or self._client.data_converter + )._with_store_context(self._get_async_activity_store_context(input.id_or_token)) + details = ( + None + if not input.details + else await data_converter.encode_wrapper(input.details) + ) + if isinstance(input.id_or_token, AsyncActivityIDReference): + await self._client.workflow_service.respond_activity_task_canceled_by_id( + temporalio.api.workflowservice.v1.RespondActivityTaskCanceledByIdRequest( + workflow_id=input.id_or_token.workflow_id or "", + run_id=input.id_or_token.run_id or "", + activity_id=input.id_or_token.activity_id, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + else: + await self._client.workflow_service.respond_activity_task_canceled( + temporalio.api.workflowservice.v1.RespondActivityTaskCanceledRequest( + task_token=input.id_or_token, + namespace=self._client.namespace, + identity=self._client.identity, + details=details, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + ### 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: temporalio.api.schedule.v1.SchedulePatch | None = None + if input.trigger_immediately or input.backfill: + initial_patch = temporalio.api.schedule.v1.SchedulePatch( + trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( + overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + input.schedule.policy.overlap + ), + ) + if input.trigger_immediately + else None, + backfill_request=[b._to_proto() for b in input.backfill] + if input.backfill + else None, + ) + try: + request = temporalio.api.workflowservice.v1.CreateScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + schedule=await input.schedule._to_proto(self._client), + initial_patch=initial_patch, + identity=self._client.identity, + request_id=str(uuid.uuid4()), + memo=await self._client.data_converter._encode_memo(input.memo) + if input.memo + else None, + ) + if input.search_attributes: + temporalio.converter.encode_search_attributes( + input.search_attributes, request.search_attributes + ) + await self._client.workflow_service.create_schedule( + request, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + except RPCError as err: + already_started = ( + err.status == RPCStatusCode.ALREADY_EXISTS + and err.grpc_status.details + and err.grpc_status.details[0].Is( + temporalio.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure.DESCRIPTOR + ) + ) + if already_started: + raise ScheduleAlreadyRunningError() + raise + return ScheduleHandle(self._client, input.id) + + def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: + return ScheduleAsyncIterator(self._client, input) + + async def backfill_schedule(self, input: BackfillScheduleInput) -> None: + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + backfill_request=[b._to_proto() for b in input.backfills], + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def delete_schedule(self, input: DeleteScheduleInput) -> None: + await self._client.workflow_service.delete_schedule( + temporalio.api.workflowservice.v1.DeleteScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + identity=self._client.identity, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def describe_schedule( + self, input: DescribeScheduleInput + ) -> ScheduleDescription: + return ScheduleDescription._from_proto( + input.id, + await self._client.workflow_service.describe_schedule( + temporalio.api.workflowservice.v1.DescribeScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ), + self._client.data_converter, + ) + + async def pause_schedule(self, input: PauseScheduleInput) -> None: + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + pause=input.note or "Paused via Python SDK", + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def trigger_schedule(self, input: TriggerScheduleInput) -> None: + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED + if input.overlap: + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + input.overlap + ) + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + trigger_immediately=temporalio.api.schedule.v1.TriggerImmediatelyRequest( + overlap_policy=overlap_policy, + ), + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: + await self._client.workflow_service.patch_schedule( + temporalio.api.workflowservice.v1.PatchScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + patch=temporalio.api.schedule.v1.SchedulePatch( + unpause=input.note or "Unpaused via Python SDK", + ), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def update_schedule(self, input: UpdateScheduleInput) -> None: + # TODO(cretz): This is supposed to be a retry-conflict loop, but we do + # not yet have a way to know update failure is due to conflict token + # mismatch + update = input.updater( + ScheduleUpdateInput( + description=ScheduleDescription._from_proto( + input.id, + await self._client.workflow_service.describe_schedule( + temporalio.api.workflowservice.v1.DescribeScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + ), + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ), + self._client.data_converter, + ) + ) + ) + if inspect.iscoroutine(update): + update = await update + if not update: + return + assert isinstance(update, ScheduleUpdate) + request = temporalio.api.workflowservice.v1.UpdateScheduleRequest( + namespace=self._client.namespace, + schedule_id=input.id, + schedule=await update.schedule._to_proto(self._client), + identity=self._client.identity, + request_id=str(uuid.uuid4()), + ) + if update.search_attributes is not None: + request.search_attributes.indexed_fields.clear() # Ensure that we at least create an empty map + temporalio.converter.encode_search_attributes( + update.search_attributes, request.search_attributes + ) + await self._client.workflow_service.update_schedule( + request, + retry=True, + metadata=input.rpc_metadata, + timeout=input.rpc_timeout, + ) + + async def update_worker_build_id_compatibility( + self, input: UpdateWorkerBuildIdCompatibilityInput + ) -> None: + req = input.operation._as_partial_proto() + req.namespace = self._client.namespace + req.task_queue = input.task_queue + await self._client.workflow_service.update_worker_build_id_compatibility( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + + async def get_worker_build_id_compatibility( + self, input: GetWorkerBuildIdCompatibilityInput + ) -> WorkerBuildIdVersionSets: + req = temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest( + namespace=self._client.namespace, + task_queue=input.task_queue, + max_sets=input.max_sets or 0, + ) + resp = await self._client.workflow_service.get_worker_build_id_compatibility( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + return WorkerBuildIdVersionSets._from_proto(resp) + + async def get_worker_task_reachability( + self, input: GetWorkerTaskReachabilityInput + ) -> WorkerTaskReachability: + req = temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityRequest( + namespace=self._client.namespace, + build_ids=input.build_ids, + task_queues=input.task_queues, + reachability=input.reachability._to_proto() + if input.reachability + else temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED, + ) + resp = await self._client.workflow_service.get_worker_task_reachability( + req, retry=True, metadata=input.rpc_metadata, timeout=input.rpc_timeout + ) + return WorkerTaskReachability._from_proto(resp) + + async def _apply_headers( + self, + source: Mapping[str, temporalio.api.common.v1.Payload] | None, + dest: MessageMap[str, temporalio.api.common.v1.Payload], + ) -> None: + await _apply_headers( + source, + dest, + self._client.config(active_config=True)["header_codec_behavior"] + == HeaderCodecBehavior.CODEC, + self._client.data_converter, + ) diff --git a/temporalio/client/_interceptor.py b/temporalio/client/_interceptor.py new file mode 100644 index 000000000..486c9bbd4 --- /dev/null +++ b/temporalio/client/_interceptor.py @@ -0,0 +1,783 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from collections.abc import ( + Awaitable, + Callable, + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import timedelta +from typing import ( + TYPE_CHECKING, + Any, +) + +import temporalio.api.common.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +from temporalio.converter import ( + DataConverter, +) + +from ._callback import Callback + +if TYPE_CHECKING: + from ._activity import ( + ActivityExecutionAsyncIterator, + ActivityExecutionCount, + ActivityExecutionDescription, + ActivityHandle, + AsyncActivityIDReference, + ) + from ._schedule import ( + Schedule, + ScheduleAsyncIterator, + ScheduleBackfill, + ScheduleDescription, + ScheduleHandle, + ScheduleOverlapPolicy, + ScheduleUpdate, + ScheduleUpdateInput, + ) + from ._worker_versioning import ( + BuildIdOp, + TaskReachabilityType, + WorkerBuildIdVersionSets, + WorkerTaskReachability, + ) + from ._workflow import ( + WorkflowExecutionAsyncIterator, + WorkflowExecutionCount, + WorkflowExecutionDescription, + WorkflowHandle, + WorkflowHistoryEventAsyncIterator, + WorkflowHistoryEventFilterType, + WorkflowUpdateHandle, + WorkflowUpdateStage, + ) + + +@dataclass +class StartWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.start_workflow`.""" + + workflow: str + args: Sequence[Any] + id: str + task_queue: str + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy + retry_policy: temporalio.common.RetryPolicy | None + cron_schedule: str + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + start_delay: timedelta | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + start_signal: str | None + start_signal_args: Sequence[Any] + static_summary: str | None + static_details: str | None + # Type may be absent + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + request_eager_start: bool + priority: temporalio.common.Priority + # The following options are experimental and unstable. + callbacks: Sequence[Callback] + workflow_event_links: Sequence[temporalio.api.common.v1.Link.WorkflowEvent] + request_id: str | None + versioning_override: temporalio.common.VersioningOverride | None = None + + +@dataclass +class CancelWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.cancel_workflow`.""" + + id: str + run_id: str | None + first_execution_run_id: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.describe_workflow`.""" + + id: str + run_id: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class FetchWorkflowHistoryEventsInput: + """Input for :py:meth:`OutboundInterceptor.fetch_workflow_history_events`.""" + + id: str + run_id: str | None + page_size: int | None + next_page_token: bytes | None + wait_new_event: bool + event_filter_type: WorkflowHistoryEventFilterType + skip_archival: bool + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListWorkflowsInput: + """Input for :py:meth:`OutboundInterceptor.list_workflows`.""" + + query: str | None + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + limit: int | None + + +@dataclass +class CountWorkflowsInput: + """Input for :py:meth:`OutboundInterceptor.count_workflows`.""" + + query: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class QueryWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.query_workflow`.""" + + id: str + run_id: str | None + query: str + args: Sequence[Any] + reject_condition: temporalio.common.QueryRejectCondition | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + # Type may be absent + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class SignalWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.signal_workflow`.""" + + id: str + run_id: str | None + signal: str + args: Sequence[Any] + headers: Mapping[str, temporalio.api.common.v1.Payload] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TerminateWorkflowInput: + """Input for :py:meth:`OutboundInterceptor.terminate_workflow`.""" + + id: str + run_id: str | None + first_execution_run_id: str | None + args: Sequence[Any] + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class StartActivityInput: + """Input for :py:meth:`OutboundInterceptor.start_activity`. + + .. warning:: + This API is experimental. + """ + + activity_type: str + args: Sequence[Any] + id: str + task_queue: str + result_type: type | None + schedule_to_close_timeout: timedelta | None + start_to_close_timeout: timedelta | None + schedule_to_start_timeout: timedelta | None + heartbeat_timeout: timedelta | None + id_reuse_policy: temporalio.common.ActivityIDReusePolicy + id_conflict_policy: temporalio.common.ActivityIDConflictPolicy + retry_policy: temporalio.common.RetryPolicy | None + priority: temporalio.common.Priority + search_attributes: temporalio.common.TypedSearchAttributes | None + summary: str | None + start_delay: timedelta | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class CancelActivityInput: + """Input for :py:meth:`OutboundInterceptor.cancel_activity`. + + .. warning:: + This API is experimental. + """ + + activity_id: str + activity_run_id: str | None + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TerminateActivityInput: + """Input for :py:meth:`OutboundInterceptor.terminate_activity`. + + .. warning:: + This API is experimental. + """ + + activity_id: str + activity_run_id: str | None + reason: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeActivityInput: + """Input for :py:meth:`OutboundInterceptor.describe_activity`. + + .. warning:: + This API is experimental. + """ + + activity_id: str + activity_run_id: str | None + long_poll_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListActivitiesInput: + """Input for :py:meth:`OutboundInterceptor.list_activities`. + + .. warning:: + This API is experimental. + """ + + query: str | None + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + limit: int | None + + +@dataclass +class CountActivitiesInput: + """Input for :py:meth:`OutboundInterceptor.count_activities`. + + .. warning:: + This API is experimental. + """ + + query: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class StartWorkflowUpdateInput: + """Input for :py:meth:`OutboundInterceptor.start_workflow_update`.""" + + id: str + run_id: str | None + first_execution_run_id: str | None + update_id: str | None + update: str + args: Sequence[Any] + wait_for_stage: WorkflowUpdateStage + headers: Mapping[str, temporalio.api.common.v1.Payload] + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UpdateWithStartUpdateWorkflowInput: + """Update input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" + + update_id: str | None + update: str + args: Sequence[Any] + wait_for_stage: WorkflowUpdateStage + headers: Mapping[str, temporalio.api.common.v1.Payload] + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UpdateWithStartStartWorkflowInput: + """StartWorkflow input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" + + # Similar to StartWorkflowInput but without e.g. run_id, start_signal, + # start_signal_args, request_eager_start. + + workflow: str + args: Sequence[Any] + id: str + task_queue: str + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy + retry_policy: temporalio.common.RetryPolicy | None + cron_schedule: str + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + start_delay: timedelta | None + headers: Mapping[str, temporalio.api.common.v1.Payload] + static_summary: str | None + static_details: str | None + # Type may be absent + ret_type: type | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + priority: temporalio.common.Priority + versioning_override: temporalio.common.VersioningOverride | None = None + + +@dataclass +class StartWorkflowUpdateWithStartInput: + """Input for :py:meth:`OutboundInterceptor.start_update_with_start_workflow`.""" + + start_workflow_input: UpdateWithStartStartWorkflowInput + update_workflow_input: UpdateWithStartUpdateWorkflowInput + _on_start: Callable[ + [temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse], None + ] + _on_start_error: Callable[[BaseException], None] + + +@dataclass +class HeartbeatAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.heartbeat_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + details: Sequence[Any] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class CompleteAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.complete_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + result: Any | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class FailAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.fail_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + error: Exception + last_heartbeat_details: Sequence[Any] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class ReportCancellationAsyncActivityInput: + """Input for :py:meth:`OutboundInterceptor.report_cancellation_async_activity`.""" + + id_or_token: AsyncActivityIDReference | bytes + details: Sequence[Any] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + data_converter_override: DataConverter | None = None + + +@dataclass +class CreateScheduleInput: + """Input for :py:meth:`OutboundInterceptor.create_schedule`.""" + + id: str + schedule: Schedule + trigger_immediately: bool + backfill: Sequence[ScheduleBackfill] + memo: Mapping[str, Any] | None + search_attributes: None | ( + temporalio.common.SearchAttributes | temporalio.common.TypedSearchAttributes + ) + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class ListSchedulesInput: + """Input for :py:meth:`OutboundInterceptor.list_schedules`.""" + + page_size: int + next_page_token: bytes | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + query: str | None = None + + +@dataclass +class BackfillScheduleInput: + """Input for :py:meth:`OutboundInterceptor.backfill_schedule`.""" + + id: str + backfills: Sequence[ScheduleBackfill] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DeleteScheduleInput: + """Input for :py:meth:`OutboundInterceptor.delete_schedule`.""" + + id: str + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class DescribeScheduleInput: + """Input for :py:meth:`OutboundInterceptor.describe_schedule`.""" + + id: str + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class PauseScheduleInput: + """Input for :py:meth:`OutboundInterceptor.pause_schedule`.""" + + id: str + note: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class TriggerScheduleInput: + """Input for :py:meth:`OutboundInterceptor.trigger_schedule`.""" + + id: str + overlap: ScheduleOverlapPolicy | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UnpauseScheduleInput: + """Input for :py:meth:`OutboundInterceptor.unpause_schedule`.""" + + id: str + note: str | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UpdateScheduleInput: + """Input for :py:meth:`OutboundInterceptor.update_schedule`.""" + + id: str + updater: Callable[ + [ScheduleUpdateInput], + ScheduleUpdate | None | Awaitable[ScheduleUpdate | None], + ] + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class UpdateWorkerBuildIdCompatibilityInput: + """Input for :py:meth:`OutboundInterceptor.update_worker_build_id_compatibility`.""" + + task_queue: str + operation: BuildIdOp + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class GetWorkerBuildIdCompatibilityInput: + """Input for :py:meth:`OutboundInterceptor.get_worker_build_id_compatibility`.""" + + task_queue: str + max_sets: int | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class GetWorkerTaskReachabilityInput: + """Input for :py:meth:`OutboundInterceptor.get_worker_task_reachability`.""" + + build_ids: Sequence[str] + task_queues: Sequence[str] + reachability: TaskReachabilityType | None + rpc_metadata: Mapping[str, str | bytes] + rpc_timeout: timedelta | None + + +@dataclass +class Interceptor: + """Interceptor for clients. + + This should be extended by any client interceptors. + """ + + def intercept_client(self, next: OutboundInterceptor) -> OutboundInterceptor: + """Method called for intercepting a client. + + Args: + next: The underlying outbound interceptor this interceptor should + delegate to. + + Returns: + The new interceptor that will be called for each client call. + """ + return next + + +class OutboundInterceptor: + """OutboundInterceptor for intercepting client calls. + + This should be extended by any client outbound interceptors. + """ + + def __init__(self, next: OutboundInterceptor) -> None: + """Create the outbound interceptor. + + Args: + next: The next interceptor in the chain. The default implementation + of all calls is to delegate to the next interceptor. + """ + self.next = next + + ### Workflow calls + + async def start_workflow( + self, input: StartWorkflowInput + ) -> WorkflowHandle[Any, Any]: + """Called for every :py:meth:`Client.start_workflow` call.""" + return await self.next.start_workflow(input) + + async def cancel_workflow(self, input: CancelWorkflowInput) -> None: + """Called for every :py:meth:`WorkflowHandle.cancel` call.""" + await self.next.cancel_workflow(input) + + async def describe_workflow( + self, input: DescribeWorkflowInput + ) -> WorkflowExecutionDescription: + """Called for every :py:meth:`WorkflowHandle.describe` call.""" + return await self.next.describe_workflow(input) + + def fetch_workflow_history_events( + self, input: FetchWorkflowHistoryEventsInput + ) -> WorkflowHistoryEventAsyncIterator: + """Called for every :py:meth:`WorkflowHandle.fetch_history_events` call.""" + return self.next.fetch_workflow_history_events(input) + + def list_workflows( + self, input: ListWorkflowsInput + ) -> WorkflowExecutionAsyncIterator: + """Called for every :py:meth:`Client.list_workflows` call.""" + return self.next.list_workflows(input) + + async def count_workflows( + self, input: CountWorkflowsInput + ) -> WorkflowExecutionCount: + """Called for every :py:meth:`Client.count_workflows` call.""" + return await self.next.count_workflows(input) + + async def query_workflow(self, input: QueryWorkflowInput) -> Any: + """Called for every :py:meth:`WorkflowHandle.query` call.""" + return await self.next.query_workflow(input) + + async def signal_workflow(self, input: SignalWorkflowInput) -> None: + """Called for every :py:meth:`WorkflowHandle.signal` call.""" + await self.next.signal_workflow(input) + + async def terminate_workflow(self, input: TerminateWorkflowInput) -> None: + """Called for every :py:meth:`WorkflowHandle.terminate` call.""" + await self.next.terminate_workflow(input) + + ### Activity calls + + async def start_activity(self, input: StartActivityInput) -> ActivityHandle[Any]: + """Called for every :py:meth:`Client.start_activity` call. + + .. warning:: + This API is experimental. + """ + return await self.next.start_activity(input) + + async def cancel_activity(self, input: CancelActivityInput) -> None: + """Called for every :py:meth:`ActivityHandle.cancel` call. + + .. warning:: + This API is experimental. + """ + await self.next.cancel_activity(input) + + async def terminate_activity(self, input: TerminateActivityInput) -> None: + """Called for every :py:meth:`ActivityHandle.terminate` call. + + .. warning:: + This API is experimental. + """ + await self.next.terminate_activity(input) + + async def describe_activity( + self, input: DescribeActivityInput + ) -> ActivityExecutionDescription: + """Called for every :py:meth:`ActivityHandle.describe` call. + + .. warning:: + This API is experimental. + """ + return await self.next.describe_activity(input) + + def list_activities( + self, input: ListActivitiesInput + ) -> ActivityExecutionAsyncIterator: + """Called for every :py:meth:`Client.list_activities` call. + + .. warning:: + This API is experimental. + """ + return self.next.list_activities(input) + + async def count_activities( + self, input: CountActivitiesInput + ) -> ActivityExecutionCount: + """Called for every :py:meth:`Client.count_activities` call. + + .. warning:: + This API is experimental. + """ + return await self.next.count_activities(input) + + async def start_workflow_update( + self, input: StartWorkflowUpdateInput + ) -> WorkflowUpdateHandle[Any]: + """Called for every :py:meth:`WorkflowHandle.start_update` and :py:meth:`WorkflowHandle.execute_update` call.""" + return await self.next.start_workflow_update(input) + + async def start_update_with_start_workflow( + self, input: StartWorkflowUpdateWithStartInput + ) -> WorkflowUpdateHandle[Any]: + """Called for every :py:meth:`Client.start_update_with_start_workflow` and :py:meth:`Client.execute_update_with_start_workflow` call.""" + return await self.next.start_update_with_start_workflow(input) + + ### Async activity calls + + async def heartbeat_async_activity( + self, input: HeartbeatAsyncActivityInput + ) -> None: + """Called for every :py:meth:`AsyncActivityHandle.heartbeat` call.""" + await self.next.heartbeat_async_activity(input) + + async def complete_async_activity(self, input: CompleteAsyncActivityInput) -> None: + """Called for every :py:meth:`AsyncActivityHandle.complete` call.""" + await self.next.complete_async_activity(input) + + async def fail_async_activity(self, input: FailAsyncActivityInput) -> None: + """Called for every :py:meth:`AsyncActivityHandle.fail` call.""" + await self.next.fail_async_activity(input) + + async def report_cancellation_async_activity( + self, input: ReportCancellationAsyncActivityInput + ) -> None: + """Called for every :py:meth:`AsyncActivityHandle.report_cancellation` call.""" + await self.next.report_cancellation_async_activity(input) + + ### Schedule calls + + async def create_schedule(self, input: CreateScheduleInput) -> ScheduleHandle: + """Called for every :py:meth:`Client.create_schedule` call.""" + return await self.next.create_schedule(input) + + def list_schedules(self, input: ListSchedulesInput) -> ScheduleAsyncIterator: + """Called for every :py:meth:`Client.list_schedules` call.""" + return self.next.list_schedules(input) + + async def backfill_schedule(self, input: BackfillScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.backfill` call.""" + await self.next.backfill_schedule(input) + + async def delete_schedule(self, input: DeleteScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.delete` call.""" + await self.next.delete_schedule(input) + + async def describe_schedule( + self, input: DescribeScheduleInput + ) -> ScheduleDescription: + """Called for every :py:meth:`ScheduleHandle.describe` call.""" + return await self.next.describe_schedule(input) + + async def pause_schedule(self, input: PauseScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.pause` call.""" + await self.next.pause_schedule(input) + + async def trigger_schedule(self, input: TriggerScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.trigger` call.""" + await self.next.trigger_schedule(input) + + async def unpause_schedule(self, input: UnpauseScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.unpause` call.""" + await self.next.unpause_schedule(input) + + async def update_schedule(self, input: UpdateScheduleInput) -> None: + """Called for every :py:meth:`ScheduleHandle.update` call.""" + await self.next.update_schedule(input) + + async def update_worker_build_id_compatibility( + self, input: UpdateWorkerBuildIdCompatibilityInput + ) -> None: + """Called for every :py:meth:`Client.update_worker_build_id_compatibility` call.""" + await self.next.update_worker_build_id_compatibility(input) + + async def get_worker_build_id_compatibility( + self, input: GetWorkerBuildIdCompatibilityInput + ) -> WorkerBuildIdVersionSets: + """Called for every :py:meth:`Client.get_worker_build_id_compatibility` call.""" + return await self.next.get_worker_build_id_compatibility(input) + + async def get_worker_task_reachability( + self, input: GetWorkerTaskReachabilityInput + ) -> WorkerTaskReachability: + """Called for every :py:meth:`Client.get_worker_task_reachability` call.""" + return await self.next.get_worker_task_reachability(input) diff --git a/temporalio/client/_plugin.py b/temporalio/client/_plugin.py new file mode 100644 index 000000000..95b47e43d --- /dev/null +++ b/temporalio/client/_plugin.py @@ -0,0 +1,75 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import abc +from abc import abstractmethod +from collections.abc import ( + Awaitable, + Callable, +) +from typing import ( + TYPE_CHECKING, +) + +from temporalio.service import ( + ConnectConfig, + ServiceClient, +) + +if TYPE_CHECKING: + from ._client import ClientConfig + + +class Plugin(abc.ABC): + """Base class for client plugins that can intercept and modify client behavior. + + Plugins allow customization of client creation and service connection processes + through a chain of responsibility pattern. Each plugin can modify the client + configuration or intercept service client connections. + + If the plugin is also a temporalio.worker.Plugin, it will additionally be propagated as a worker plugin. + You should likley not also provide it to the worker as that will result in the plugin being applied twice. + """ + + def name(self) -> str: + """Get the name of this plugin. Can be overridden if desired to provide a more appropriate name. + + Returns: + The fully qualified name of the plugin class (module.classname). + """ + return type(self).__module__ + "." + type(self).__qualname__ + + @abstractmethod + def configure_client(self, config: ClientConfig) -> ClientConfig: + """Hook called when creating a client to allow modification of configuration. + + This method is called during client creation and allows plugins to modify + the client configuration before the client is fully initialized. Plugins + can add interceptors, modify connection parameters, or change other settings. + + Args: + config: The client configuration dictionary to potentially modify. + + Returns: + The modified client configuration. + """ + + @abstractmethod + async def connect_service_client( + self, + config: ConnectConfig, + next: Callable[[ConnectConfig], Awaitable[ServiceClient]], + ) -> ServiceClient: + """Hook called when connecting to the Temporal service. + + This method is called during service client connection and allows plugins + to intercept or modify the connection process. Plugins can modify connection + parameters, add authentication, or provide custom connection logic. + + Args: + config: The service connection configuration. + + Returns: + The connected service client. + """ diff --git a/temporalio/client/_schedule.py b/temporalio/client/_schedule.py new file mode 100644 index 000000000..15946cfe8 --- /dev/null +++ b/temporalio/client/_schedule.py @@ -0,0 +1,1604 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import dataclasses +from abc import ABC, abstractmethod +from collections.abc import ( + Awaitable, + Callable, + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import IntEnum +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + overload, +) + +import google.protobuf.duration_pb2 +import google.protobuf.timestamp_pb2 + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.schedule.v1 +import temporalio.api.taskqueue.v1 +import temporalio.api.workflow.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.workflow +from temporalio.converter import ( + StorageDriverStoreContext, + StorageDriverWorkflowInfo, + WorkflowSerializationContext, +) + +from ..common import HeaderCodecBehavior +from ..types import ( + AnyType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._helpers import _apply_headers, _encode_user_metadata +from ._interceptor import ( + BackfillScheduleInput, + DeleteScheduleInput, + DescribeScheduleInput, + PauseScheduleInput, + TriggerScheduleInput, + UnpauseScheduleInput, + UpdateScheduleInput, +) + +if TYPE_CHECKING: + from ._client import Client + from ._interceptor import ListSchedulesInput + + +class ScheduleHandle: + """Handle for interacting with a schedule. + + This is usually created via :py:meth:`Client.get_schedule_handle` or + returned from :py:meth:`Client.create_schedule`. + + Attributes: + id: ID of the schedule. + """ + + def __init__(self, client: Client, id: str) -> None: + """Create schedule handle.""" + self._client = client + self.id = id + + async def backfill( + self, + *backfill: ScheduleBackfill, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Backfill the schedule by going through the specified time periods as + if they passed right now. + + Args: + backfill: Backfill periods. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + if not backfill: + raise ValueError("At least one backfill required") + await self._client._impl.backfill_schedule( + BackfillScheduleInput( + id=self.id, + backfills=backfill, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def delete( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Delete this schedule. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.delete_schedule( + DeleteScheduleInput( + id=self.id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def describe( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ScheduleDescription: + """Fetch this schedule's description. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + return await self._client._impl.describe_schedule( + DescribeScheduleInput( + id=self.id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def pause( + self, + *, + note: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Pause the schedule and set a note. + + Args: + note: Note to set on the schedule. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.pause_schedule( + PauseScheduleInput( + id=self.id, + note=note, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def trigger( + self, + *, + overlap: ScheduleOverlapPolicy | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Trigger an action on this schedule to happen immediately. + + Args: + overlap: If set, overrides the schedule's overlap policy. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.trigger_schedule( + TriggerScheduleInput( + id=self.id, + overlap=overlap, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + async def unpause( + self, + *, + note: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Unpause the schedule and set a note. + + Args: + note: Note to set on the schedule. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + """ + await self._client._impl.unpause_schedule( + UnpauseScheduleInput( + id=self.id, + note=note, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + @overload + async def update( + self, + updater: Callable[[ScheduleUpdateInput], ScheduleUpdate | None], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + @overload + async def update( + self, + updater: Callable[[ScheduleUpdateInput], Awaitable[ScheduleUpdate | None]], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + async def update( + self, + updater: Callable[ + [ScheduleUpdateInput], + ScheduleUpdate | None | Awaitable[ScheduleUpdate | None], + ], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Update a schedule using a callback to build the update from the + description. + + The callback may be invoked multiple times in a conflict-resolution + loop. + + Args: + updater: Callback that returns the update. It accepts a + :py:class:`ScheduleUpdateInput` and returns a + :py:class:`ScheduleUpdate`. If None is returned or an error + occurs, the update is not attempted. This may be called multiple + times. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. This is for every call made + within. + rpc_timeout: Optional RPC deadline to set for the RPC call. This is + for each call made within, not overall. + """ + await self._client._impl.update_schedule( + UpdateScheduleInput( + id=self.id, + updater=updater, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ), + ) + + +@dataclass +class ScheduleSpec: + """Specification of the times scheduled actions may occur. + + The times are the union of :py:attr:`calendars`, :py:attr:`intervals`, and + :py:attr:`cron_expressions` excluding anything in :py:attr:`skip`. + """ + + calendars: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) + """Calendar-based specification of times.""" + + intervals: Sequence[ScheduleIntervalSpec] = dataclasses.field(default_factory=list) + """Interval-based specification of times.""" + + cron_expressions: Sequence[str] = dataclasses.field(default_factory=list) + """Cron-based specification of times. + + This is provided for easy migration from legacy string-based cron + scheduling. New uses should use :py:attr:`calendars` instead. These + expressions will be translated to calendar-based specifications on the + server. + """ + + skip: Sequence[ScheduleCalendarSpec] = dataclasses.field(default_factory=list) + """Set of matching calendar times that will be skipped.""" + + start_at: datetime | None = None + """Time before which any matching times will be skipped.""" + + end_at: datetime | None = None + """Time after which any matching times will be skipped.""" + + jitter: timedelta | None = None + """Jitter to apply each action. + + An action's scheduled time will be incremented by a random value between 0 + and this value if present (but not past the next schedule). + """ + + time_zone_name: str | None = None + """IANA time zone name, for example ``US/Central``.""" + + @staticmethod + def _from_proto(spec: temporalio.api.schedule.v1.ScheduleSpec) -> ScheduleSpec: + return ScheduleSpec( + calendars=[ + ScheduleCalendarSpec._from_proto(c) for c in spec.structured_calendar + ], + intervals=[ScheduleIntervalSpec._from_proto(i) for i in spec.interval], + cron_expressions=spec.cron_string, + skip=[ + ScheduleCalendarSpec._from_proto(c) + for c in spec.exclude_structured_calendar + ], + start_at=spec.start_time.ToDatetime().replace(tzinfo=timezone.utc) + if spec.HasField("start_time") + else None, + end_at=spec.end_time.ToDatetime().replace(tzinfo=timezone.utc) + if spec.HasField("end_time") + else None, + jitter=spec.jitter.ToTimedelta() if spec.HasField("jitter") else None, + time_zone_name=spec.timezone_name or None, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleSpec: + start_time: google.protobuf.timestamp_pb2.Timestamp | None = None + if self.start_at: + start_time = google.protobuf.timestamp_pb2.Timestamp() + start_time.FromDatetime(self.start_at) + end_time: google.protobuf.timestamp_pb2.Timestamp | None = None + if self.end_at: + end_time = google.protobuf.timestamp_pb2.Timestamp() + end_time.FromDatetime(self.end_at) + jitter: google.protobuf.duration_pb2.Duration | None = None + if self.jitter: + jitter = google.protobuf.duration_pb2.Duration() + jitter.FromTimedelta(self.jitter) + return temporalio.api.schedule.v1.ScheduleSpec( + structured_calendar=[cal._to_proto() for cal in self.calendars], + cron_string=self.cron_expressions, + interval=[i._to_proto() for i in self.intervals], + exclude_structured_calendar=[cal._to_proto() for cal in self.skip], + start_time=start_time, + end_time=end_time, + jitter=jitter, + timezone_name=self.time_zone_name or "", + ) + + +@dataclass(frozen=True) +class ScheduleRange: + """Inclusive range for a schedule match value.""" + + start: int + """Inclusive start of the range.""" + + end: int = 0 + """Inclusive end of the range. + + If unset or less than start, defaults to start. + """ + + step: int = 0 + """ + Step to take between each value. + + Unset or 0 defaults as 1. + """ + + def __post_init__(self): + """Set field defaults.""" + # Class is frozen, so we must setattr bypassing dataclass setattr + if self.end < self.start: + object.__setattr__(self, "end", self.start) + if self.step == 0: + object.__setattr__(self, "step", 1) + + @staticmethod + def _from_protos( + ranges: Sequence[temporalio.api.schedule.v1.Range], + ) -> Sequence[ScheduleRange]: + return tuple(ScheduleRange._from_proto(r) for r in ranges) + + @staticmethod + def _from_proto(range: temporalio.api.schedule.v1.Range) -> ScheduleRange: + return ScheduleRange(start=range.start, end=range.end, step=range.step) + + @staticmethod + def _to_protos( + ranges: Sequence[ScheduleRange], + ) -> Sequence[temporalio.api.schedule.v1.Range]: + return tuple(r._to_proto() for r in ranges) + + def _to_proto(self) -> temporalio.api.schedule.v1.Range: + return temporalio.api.schedule.v1.Range( + start=self.start, end=self.end, step=self.step + ) + + +@dataclass +class ScheduleCalendarSpec: + """Specification relative to calendar time when to run an action. + + 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. + """ + + second: Sequence[ScheduleRange] = (ScheduleRange(0),) + """Second range to match, 0-59. Default matches 0.""" + + minute: Sequence[ScheduleRange] = (ScheduleRange(0),) + """Minute range to match, 0-59. Default matches 0.""" + + hour: Sequence[ScheduleRange] = (ScheduleRange(0),) + """Hour range to match, 0-23. Default matches 0.""" + + day_of_month: Sequence[ScheduleRange] = (ScheduleRange(1, 31),) + """Day of month range to match, 1-31. Default matches all days.""" + + month: Sequence[ScheduleRange] = (ScheduleRange(1, 12),) + """Month range to match, 1-12. Default matches all months.""" + + year: Sequence[ScheduleRange] = () + """Optional year range to match. Default of empty matches all years.""" + + day_of_week: Sequence[ScheduleRange] = (ScheduleRange(0, 6),) + """Day of week range to match, 0-6, 0 is Sunday. Default matches all + days.""" + + comment: str | None = None + """Description of this schedule.""" + + @staticmethod + def _from_proto( + spec: temporalio.api.schedule.v1.StructuredCalendarSpec, + ) -> ScheduleCalendarSpec: + return ScheduleCalendarSpec( + second=ScheduleRange._from_protos(spec.second), + minute=ScheduleRange._from_protos(spec.minute), + hour=ScheduleRange._from_protos(spec.hour), + day_of_month=ScheduleRange._from_protos(spec.day_of_month), + month=ScheduleRange._from_protos(spec.month), + year=ScheduleRange._from_protos(spec.year), + day_of_week=ScheduleRange._from_protos(spec.day_of_week), + comment=spec.comment or None, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.StructuredCalendarSpec: + return temporalio.api.schedule.v1.StructuredCalendarSpec( + second=ScheduleRange._to_protos(self.second), + minute=ScheduleRange._to_protos(self.minute), + hour=ScheduleRange._to_protos(self.hour), + day_of_month=ScheduleRange._to_protos(self.day_of_month), + month=ScheduleRange._to_protos(self.month), + year=ScheduleRange._to_protos(self.year), + day_of_week=ScheduleRange._to_protos(self.day_of_week), + comment=self.comment or "", + ) + + +@dataclass +class ScheduleIntervalSpec: + """Specification for scheduling on an interval. + + Matches times expressed as epoch + (n * every) + offset. + """ + + every: timedelta + """Period to repeat the interval.""" + + offset: timedelta | None = None + """Fixed offset added to each interval period.""" + + @staticmethod + def _from_proto( + spec: temporalio.api.schedule.v1.IntervalSpec, + ) -> ScheduleIntervalSpec: + return ScheduleIntervalSpec( + every=spec.interval.ToTimedelta(), + offset=spec.phase.ToTimedelta() if spec.HasField("phase") else None, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.IntervalSpec: + interval = google.protobuf.duration_pb2.Duration() + interval.FromTimedelta(self.every) + phase: google.protobuf.duration_pb2.Duration | None = None + if self.offset: + phase = google.protobuf.duration_pb2.Duration() + phase.FromTimedelta(self.offset) + return temporalio.api.schedule.v1.IntervalSpec(interval=interval, phase=phase) + + +class ScheduleAction(ABC): + """Base class for an action a schedule can take. + + See :py:class:`ScheduleActionStartWorkflow` for the most commonly used + implementation. + """ + + @staticmethod + def _from_proto( + action: temporalio.api.schedule.v1.ScheduleAction, + ) -> ScheduleAction: + if action.HasField("start_workflow"): + return ScheduleActionStartWorkflow._from_proto(action.start_workflow) + else: + raise ValueError(f"Unsupported action: {action.WhichOneof('action')}") + + @abstractmethod + async def _to_proto( + self, client: Client + ) -> temporalio.api.schedule.v1.ScheduleAction: ... + + +@dataclass +class ScheduleActionStartWorkflow(ScheduleAction): + """Schedule action to start a workflow.""" + + workflow: str + args: Sequence[Any] | Sequence[temporalio.api.common.v1.Payload] + id: str + task_queue: str + execution_timeout: timedelta | None + run_timeout: timedelta | None + task_timeout: timedelta | None + retry_policy: temporalio.common.RetryPolicy | None + memo: None | (Mapping[str, Any] | Mapping[str, temporalio.api.common.v1.Payload]) + typed_search_attributes: temporalio.common.TypedSearchAttributes + untyped_search_attributes: temporalio.common.SearchAttributes + """This is deprecated and is only present in case existing untyped + attributes already exist for update. This should never be used when + creating.""" + static_summary: str | temporalio.api.common.v1.Payload | None + static_details: str | temporalio.api.common.v1.Payload | None + priority: temporalio.common.Priority + + headers: Mapping[str, temporalio.api.common.v1.Payload] | None + """ + Headers may still be encoded by the payload codec if present. + """ + _from_raw: bool = dataclasses.field(compare=False, init=False) + + @staticmethod + def _from_proto( # pyright: ignore + info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, # type: ignore[override] + ) -> ScheduleActionStartWorkflow: + return ScheduleActionStartWorkflow("", raw_info=info) + + # Overload for no-param workflow + @overload + def __init__( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for single-param workflow + @overload + def __init__( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for multi-param workflow + @overload + def __init__( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for string-name workflow + @overload + def __init__( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + static_summary: str | None = None, + static_details: str | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: ... + + # Overload for raw info + @overload + def __init__( + self, + workflow: str, + *, + raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo, + ) -> None: ... + + def __init__( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + task_queue: str | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + retry_policy: temporalio.common.RetryPolicy | None = None, + memo: Mapping[str, Any] | None = None, + typed_search_attributes: temporalio.common.TypedSearchAttributes = temporalio.common.TypedSearchAttributes.empty, + untyped_search_attributes: temporalio.common.SearchAttributes = {}, + static_summary: str | None = None, + static_details: str | None = None, + headers: Mapping[str, temporalio.api.common.v1.Payload] | None = None, + raw_info: temporalio.api.workflow.v1.NewWorkflowExecutionInfo | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + ) -> None: + """Create a start-workflow action. + + See :py:meth:`Client.start_workflow` for details on these parameter + values. + """ + super().__init__() + if raw_info: + self._from_raw = True + # Ignore other fields + self.workflow = raw_info.workflow_type.name + self.args = raw_info.input.payloads if raw_info.input else [] + self.id = raw_info.workflow_id + self.task_queue = raw_info.task_queue.name + self.execution_timeout = ( + raw_info.workflow_execution_timeout.ToTimedelta() + if raw_info.HasField("workflow_execution_timeout") + else None + ) + self.run_timeout = ( + raw_info.workflow_run_timeout.ToTimedelta() + if raw_info.HasField("workflow_run_timeout") + else None + ) + self.task_timeout = ( + raw_info.workflow_task_timeout.ToTimedelta() + if raw_info.HasField("workflow_task_timeout") + else None + ) + self.retry_policy = ( + temporalio.common.RetryPolicy.from_proto(raw_info.retry_policy) + if raw_info.HasField("retry_policy") + else None + ) + self.memo = raw_info.memo.fields if raw_info.memo.fields else None + self.typed_search_attributes = ( + temporalio.converter.decode_typed_search_attributes( + raw_info.search_attributes + ) + ) + self.headers = raw_info.header.fields if raw_info.header.fields else None + # Also set the untyped attributes as the set of attributes from + # decode with the typed ones removed + self.untyped_search_attributes = ( + temporalio.converter.decode_search_attributes( + raw_info.search_attributes + ) + ) + for pair in self.typed_search_attributes: + if pair.key.name in self.untyped_search_attributes: + # We know this is mutable here + del self.untyped_search_attributes[pair.key.name] # type: ignore + self.static_summary = ( + raw_info.user_metadata.summary + if raw_info.HasField("user_metadata") and raw_info.user_metadata.summary + else None + ) + self.static_details = ( + raw_info.user_metadata.details + if raw_info.HasField("user_metadata") and raw_info.user_metadata.details + else None + ) + self.priority = ( + temporalio.common.Priority._from_proto(raw_info.priority) + if raw_info.HasField("priority") and raw_info.priority + else temporalio.common.Priority.default + ) + else: + self._from_raw = False + if not id: + raise ValueError("ID required") + if not task_queue: + raise ValueError("Task queue required") + # Use definition if callable + if callable(workflow): + defn = temporalio.workflow._Definition.must_from_run_fn(workflow) + if not defn.name: + raise ValueError("Cannot schedule dynamic workflow explicitly") + workflow = defn.name + elif not isinstance(workflow, str): + raise TypeError("Workflow must be a string or callable") # type:ignore[reportUnreachable] + self.workflow = workflow + self.args = temporalio.common._arg_or_args(arg, args) + self.id = id + self.task_queue = task_queue + self.execution_timeout = execution_timeout + self.run_timeout = run_timeout + self.task_timeout = task_timeout + self.retry_policy = retry_policy + self.memo = memo + self.typed_search_attributes = typed_search_attributes + self.untyped_search_attributes = untyped_search_attributes + self.headers = headers # encode here + self.static_summary = static_summary + self.static_details = static_details + self.priority = priority + + async def _to_proto( + self, client: Client + ) -> temporalio.api.schedule.v1.ScheduleAction: + execution_timeout: google.protobuf.duration_pb2.Duration | None = None + if self.execution_timeout: + execution_timeout = google.protobuf.duration_pb2.Duration() + execution_timeout.FromTimedelta(self.execution_timeout) + run_timeout: google.protobuf.duration_pb2.Duration | None = None + if self.run_timeout: + run_timeout = google.protobuf.duration_pb2.Duration() + run_timeout.FromTimedelta(self.run_timeout) + task_timeout: google.protobuf.duration_pb2.Duration | None = None + if self.task_timeout: + task_timeout = google.protobuf.duration_pb2.Duration() + task_timeout.FromTimedelta(self.task_timeout) + retry_policy: temporalio.api.common.v1.RetryPolicy | None = None + if self.retry_policy: + retry_policy = temporalio.api.common.v1.RetryPolicy() + self.retry_policy.apply_to_proto(retry_policy) + priority: temporalio.api.common.v1.Priority | None = None + if self.priority: + priority = self.priority._to_proto() + data_converter = client.data_converter._with_contexts( + WorkflowSerializationContext( + namespace=client.namespace, + workflow_id=self.id, + ), + StorageDriverStoreContext( + target=StorageDriverWorkflowInfo( + id=self.id, type=self.workflow, namespace=client.namespace + ), + ), + ) + action = temporalio.api.schedule.v1.ScheduleAction( + start_workflow=temporalio.api.workflow.v1.NewWorkflowExecutionInfo( + workflow_id=self.id, + workflow_type=temporalio.api.common.v1.WorkflowType(name=self.workflow), + task_queue=temporalio.api.taskqueue.v1.TaskQueue(name=self.task_queue), + input=( + temporalio.api.common.v1.Payloads( + payloads=[ + a + if isinstance(a, temporalio.api.common.v1.Payload) + else (await data_converter.encode([a]))[0] + for a in self.args + ] + ) + if self.args + else None + ), + workflow_execution_timeout=execution_timeout, + workflow_run_timeout=run_timeout, + workflow_task_timeout=task_timeout, + retry_policy=retry_policy, + memo=await data_converter._encode_memo(self.memo) + if self.memo + else None, + user_metadata=await _encode_user_metadata( + data_converter, self.static_summary, self.static_details + ), + priority=priority, + ), + ) + # Add any untyped attributes that are not also in the typed set + untyped_not_in_typed = { + k: v + for k, v in self.untyped_search_attributes.items() + if k not in self.typed_search_attributes + } + if untyped_not_in_typed: + temporalio.converter.encode_search_attributes( + untyped_not_in_typed, action.start_workflow.search_attributes + ) + # TODO (dan): confirm whether this be `is not None` + if self.typed_search_attributes: + temporalio.converter.encode_search_attributes( + self.typed_search_attributes, + action.start_workflow.search_attributes, + ) + if self.headers: + await _apply_headers( + self.headers, + action.start_workflow.header.fields, + client.config(active_config=True)["header_codec_behavior"] + == HeaderCodecBehavior.CODEC + and not self._from_raw, + client.data_converter, + ) + return action + + +class ScheduleOverlapPolicy(IntEnum): + """Controls what happens when a workflow would be started by a schedule but + one is already running. + """ + + SKIP = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP + ) + """Don't start anything. + + When the workflow completes, the next scheduled event after that time will + be considered. + """ + + BUFFER_ONE = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ONE + ) + """Start the workflow again soon as the current one completes, but only + buffer one start in this way. + + If another start is supposed to happen when the workflow is running, and one + is already buffered, then only the first one will be started after the + running workflow finishes. + """ + + BUFFER_ALL = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER_ALL + ) + """Buffer up any number of starts to all happen sequentially, immediately + after the running workflow completes.""" + + CANCEL_OTHER = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER + ) + """If there is another workflow running, cancel it, and start the new one + after the old one completes cancellation.""" + + TERMINATE_OTHER = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER + ) + """If there is another workflow running, terminate it and start the new one + immediately.""" + + ALLOW_ALL = int( + temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_ALLOW_ALL + ) + """Start any number of concurrent workflows. + + Note that with this policy, last completion result and last failure will not + be available since workflows are not sequential.""" + + +@dataclass +class ScheduleBackfill: + """Time period and policy for actions taken as if the time passed right + now. + """ + + start_at: datetime + """Start of the range to evaluate the schedule in. + + This is exclusive + """ + end_at: datetime + overlap: ScheduleOverlapPolicy | None = None + + def _to_proto(self) -> temporalio.api.schedule.v1.BackfillRequest: + start_time = google.protobuf.timestamp_pb2.Timestamp() + start_time.FromDatetime(self.start_at) + end_time = google.protobuf.timestamp_pb2.Timestamp() + end_time.FromDatetime(self.end_at) + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED + if self.overlap: + overlap_policy = temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + self.overlap + ) + return temporalio.api.schedule.v1.BackfillRequest( + start_time=start_time, + end_time=end_time, + overlap_policy=overlap_policy, + ) + + +@dataclass +class SchedulePolicy: + """Policies of a schedule.""" + + overlap: ScheduleOverlapPolicy = dataclasses.field( + default_factory=lambda: ScheduleOverlapPolicy.SKIP + ) + """Controls what happens when an action is started while another is still + running.""" + + catchup_window: timedelta = timedelta(days=365) + """After a Temporal server is unavailable, amount of time in the past to + execute missed actions.""" + + pause_on_failure: bool = False + """Whether to pause the schedule if an action fails or times out. + + Note: For workflows, this only applies after all retries have been + exhausted. + """ + + @staticmethod + def _from_proto(pol: temporalio.api.schedule.v1.SchedulePolicies) -> SchedulePolicy: + return SchedulePolicy( + overlap=ScheduleOverlapPolicy(int(pol.overlap_policy)), + catchup_window=pol.catchup_window.ToTimedelta(), + pause_on_failure=pol.pause_on_failure, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.SchedulePolicies: + catchup_window = google.protobuf.duration_pb2.Duration() + catchup_window.FromTimedelta(self.catchup_window) + return temporalio.api.schedule.v1.SchedulePolicies( + overlap_policy=temporalio.api.enums.v1.ScheduleOverlapPolicy.ValueType( + self.overlap + ), + catchup_window=catchup_window, + pause_on_failure=self.pause_on_failure, + ) + + +@dataclass +class ScheduleState: + """State of a schedule.""" + + note: str | None = None + """Human readable message for the schedule. + + The system may overwrite this value on certain conditions like + pause-on-failure. + """ + + paused: bool = False + """Whether the schedule is paused.""" + + # Cannot be set to True on create + limited_actions: bool = False + """ + If true, remaining actions will be decremented for each action taken. + + 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 + """Actions remaining on this schedule. + + Once this number hits 0, no further actions are scheduled automatically. + """ + + @staticmethod + def _from_proto(state: temporalio.api.schedule.v1.ScheduleState) -> ScheduleState: + return ScheduleState( + note=state.notes or None, + paused=state.paused, + limited_actions=state.limited_actions, + remaining_actions=state.remaining_actions, + ) + + def _to_proto(self) -> temporalio.api.schedule.v1.ScheduleState: + return temporalio.api.schedule.v1.ScheduleState( + notes=self.note or "", + paused=self.paused, + limited_actions=self.limited_actions, + remaining_actions=self.remaining_actions, + ) + + +@dataclass +class Schedule: + """A schedule for periodically running an action.""" + + action: ScheduleAction + """Action taken when scheduled.""" + + spec: ScheduleSpec + """When the action is taken.""" + + policy: SchedulePolicy = dataclasses.field(default_factory=SchedulePolicy) + """Schedule policies.""" + + state: ScheduleState = dataclasses.field(default_factory=ScheduleState) + """State of the schedule.""" + + @staticmethod + def _from_proto(sched: temporalio.api.schedule.v1.Schedule) -> Schedule: + return Schedule( + action=ScheduleAction._from_proto(sched.action), + spec=ScheduleSpec._from_proto(sched.spec), + policy=SchedulePolicy._from_proto(sched.policies), + state=ScheduleState._from_proto(sched.state), + ) + + async def _to_proto(self, client: Client) -> temporalio.api.schedule.v1.Schedule: + catchup_window = google.protobuf.duration_pb2.Duration() + catchup_window.FromTimedelta(self.policy.catchup_window) + return temporalio.api.schedule.v1.Schedule( + spec=self.spec._to_proto(), + action=await self.action._to_proto(client), + policies=self.policy._to_proto(), + state=self.state._to_proto(), + ) + + +@dataclass +class ScheduleDescription: + """Description of a schedule.""" + + id: str + """ID of the schedule.""" + + schedule: Schedule + """Schedule details that can be mutated.""" + + info: ScheduleInfo + """Information about the schedule.""" + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Search attributes on the schedule.""" + + search_attributes: temporalio.common.SearchAttributes + """Search attributes on the schedule. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + data_converter: temporalio.converter.DataConverter + """Data converter used for memo decoding.""" + + raw_description: temporalio.api.workflowservice.v1.DescribeScheduleResponse + """Raw description of the schedule.""" + + @staticmethod + def _from_proto( + id: str, + desc: temporalio.api.workflowservice.v1.DescribeScheduleResponse, + converter: temporalio.converter.DataConverter, + ) -> ScheduleDescription: + return ScheduleDescription( + id=id, + schedule=Schedule._from_proto(desc.schedule), + info=ScheduleInfo._from_proto(desc.info), + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + desc.search_attributes + ), + search_attributes=temporalio.converter.decode_search_attributes( + desc.search_attributes + ), + data_converter=converter, + raw_description=desc, + ) + + async def memo(self) -> Mapping[str, Any]: + """Schedule's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come + back. For example, if the memo was originally created with a dataclass, + the value will be a dict. To convert using proper type hints, use + :py:meth:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return await self.data_converter._decode_memo(self.raw_description.memo) + + @overload + async def memo_value( + self, key: str, default: Any = temporalio.common._arg_unset + ) -> Any: ... + + @overload + async def memo_value( + self, key: str, *, type_hint: type[ParamType] + ) -> ParamType: ... + + @overload + async def memo_value( + self, key: str, default: AnyType, *, type_hint: type[ParamType] + ) -> AnyType | ParamType: ... + + async def memo_value( + self, + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, + ) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return await self.data_converter._decode_memo_field( + self.raw_description.memo, key, default, type_hint + ) + + +@dataclass +class ScheduleInfo: + """Information about a schedule.""" + + num_actions: int + """Number of actions taken by this schedule.""" + + num_actions_missed_catchup_window: int + """Number of times an action was skipped due to missing the catchup + window.""" + + num_actions_skipped_overlap: int + """Number of actions skipped due to overlap.""" + + running_actions: Sequence[ScheduleActionExecution] + """Currently running actions.""" + + recent_actions: Sequence[ScheduleActionResult] + """10 most recent actions, oldest first.""" + + next_action_times: Sequence[datetime] + """Next 10 scheduled action times.""" + + created_at: datetime + """When the schedule was created.""" + + last_updated_at: datetime | None + """When the schedule was last updated.""" + + @staticmethod + def _from_proto(info: temporalio.api.schedule.v1.ScheduleInfo) -> ScheduleInfo: + return ScheduleInfo( + num_actions=info.action_count, + num_actions_missed_catchup_window=info.missed_catchup_window, + num_actions_skipped_overlap=info.overlap_skipped, + running_actions=[ + ScheduleActionExecutionStartWorkflow._from_proto(r) + for r in info.running_workflows + ], + recent_actions=[ + ScheduleActionResult._from_proto(r) for r in info.recent_actions + ], + next_action_times=[ + f.ToDatetime().replace(tzinfo=timezone.utc) + for f in info.future_action_times + ], + created_at=info.create_time.ToDatetime().replace(tzinfo=timezone.utc), + last_updated_at=info.update_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("update_time") + else None, + ) + + +class ScheduleActionExecution(ABC): + """Base class for an action execution.""" + + pass + + +@dataclass +class ScheduleActionExecutionStartWorkflow(ScheduleActionExecution): + """Execution of a scheduled workflow start.""" + + workflow_id: str + """Workflow ID.""" + + first_execution_run_id: str + """Workflow run ID.""" + + @staticmethod + def _from_proto( + exec: temporalio.api.common.v1.WorkflowExecution, + ) -> ScheduleActionExecutionStartWorkflow: + return ScheduleActionExecutionStartWorkflow( + workflow_id=exec.workflow_id, + first_execution_run_id=exec.run_id, + ) + + +@dataclass +class ScheduleActionResult: + """Information about when an action took place.""" + + scheduled_at: datetime + """Scheduled time of the action including jitter.""" + + started_at: datetime + """When the action actually started.""" + + action: ScheduleActionExecution + """Action that took place.""" + + @staticmethod + def _from_proto( + res: temporalio.api.schedule.v1.ScheduleActionResult, + ) -> ScheduleActionResult: + return ScheduleActionResult( + scheduled_at=res.schedule_time.ToDatetime().replace(tzinfo=timezone.utc), + started_at=res.actual_time.ToDatetime().replace(tzinfo=timezone.utc), + action=ScheduleActionExecutionStartWorkflow._from_proto( + res.start_workflow_result + ), + ) + + +@dataclass +class ScheduleUpdateInput: + """Parameter for an update callback for :py:meth:`ScheduleHandle.update`.""" + + description: ScheduleDescription + """Current description of the schedule.""" + + +@dataclass +class ScheduleUpdate: + """Result of an update callback for :py:meth:`ScheduleHandle.update`.""" + + schedule: Schedule + """Schedule to update.""" + + search_attributes: temporalio.common.TypedSearchAttributes | None = None + """Search attributes to update.""" + + +@dataclass +class ScheduleListDescription: + """Description of a listed schedule.""" + + id: str + """ID of the schedule.""" + + schedule: ScheduleListSchedule | None + """Schedule details that can be mutated. + + This may not be present in older Temporal servers without advanced + visibility. + """ + + info: ScheduleListInfo | None + """Information about the schedule. + + This may not be present in older Temporal servers without advanced + visibility. + """ + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Search attributes on the schedule.""" + + search_attributes: temporalio.common.SearchAttributes + """Search attributes on the schedule. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + data_converter: temporalio.converter.DataConverter + """Data converter used for memo decoding.""" + + raw_entry: temporalio.api.schedule.v1.ScheduleListEntry + """Raw description of the schedule.""" + + @staticmethod + def _from_proto( + entry: temporalio.api.schedule.v1.ScheduleListEntry, + converter: temporalio.converter.DataConverter, + ) -> ScheduleListDescription: + return ScheduleListDescription( + id=entry.schedule_id, + schedule=ScheduleListSchedule._from_proto(entry.info) + if entry.HasField("info") + else None, + info=ScheduleListInfo._from_proto(entry.info) + if entry.HasField("info") + else None, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + entry.search_attributes + ), + search_attributes=temporalio.converter.decode_search_attributes( + entry.search_attributes + ), + data_converter=converter, + raw_entry=entry, + ) + + async def memo(self) -> Mapping[str, Any]: + """Schedule's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come + back. For example, if the memo was originally created with a dataclass, + the value will be a dict. To convert using proper type hints, use + :py:meth:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return await self.data_converter._decode_memo(self.raw_entry.memo) + + @overload + async def memo_value( + self, key: str, default: Any = temporalio.common._arg_unset + ) -> Any: ... + + @overload + async def memo_value( + self, key: str, *, type_hint: type[ParamType] + ) -> ParamType: ... + + @overload + async def memo_value( + self, key: str, default: AnyType, *, type_hint: type[ParamType] + ) -> AnyType | ParamType: ... + + async def memo_value( + self, + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, + ) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return await self.data_converter._decode_memo_field( + self.raw_entry.memo, key, default, type_hint + ) + + +@dataclass +class ScheduleListSchedule: + """Details for a listed schedule.""" + + action: ScheduleListAction + """Action taken when scheduled.""" + + spec: ScheduleSpec + """When the action is taken.""" + + state: ScheduleListState + """State of the schedule.""" + + @staticmethod + def _from_proto( + info: temporalio.api.schedule.v1.ScheduleListInfo, + ) -> ScheduleListSchedule: + # Only start workflow supported for now + if not info.HasField("workflow_type"): + raise ValueError("Unknown action on schedule") + return ScheduleListSchedule( + action=ScheduleListActionStartWorkflow(workflow=info.workflow_type.name), + spec=ScheduleSpec._from_proto(info.spec), + state=ScheduleListState._from_proto(info), + ) + + +class ScheduleListAction(ABC): + """Base class for an action a listed schedule can take.""" + + pass + + +@dataclass +class ScheduleListActionStartWorkflow(ScheduleListAction): + """Action to start a workflow on a listed schedule.""" + + workflow: str + """Workflow type name.""" + + +@dataclass +class ScheduleListInfo: + """Information about a listed schedule.""" + + recent_actions: Sequence[ScheduleActionResult] + """Most recent actions, oldest first. + + This may be a smaller amount than present on + :py:attr:`ScheduleDescription.info`. + """ + + next_action_times: Sequence[datetime] + """Next scheduled action times. + + This may be a smaller amount than present on + :py:attr:`ScheduleDescription.info`. + """ + + @staticmethod + def _from_proto( + info: temporalio.api.schedule.v1.ScheduleListInfo, + ) -> ScheduleListInfo: + return ScheduleListInfo( + recent_actions=[ + ScheduleActionResult._from_proto(r) for r in info.recent_actions + ], + next_action_times=[ + f.ToDatetime().replace(tzinfo=timezone.utc) + for f in info.future_action_times + ], + ) + + +@dataclass +class ScheduleListState: + """State of a listed schedule.""" + + note: str | None + """Human readable message for the schedule. + + The system may overwrite this value on certain conditions like + pause-on-failure. + """ + + paused: bool + """Whether the schedule is paused.""" + + @staticmethod + def _from_proto( + info: temporalio.api.schedule.v1.ScheduleListInfo, + ) -> ScheduleListState: + return ScheduleListState( + note=info.notes or None, + paused=info.paused, + ) + + +class ScheduleAsyncIterator: + """Asynchronous iterator for :py:class:`ScheduleListDescription` values. + + Most users should use ``async for`` on this iterator and not call any of the + methods within. + """ + + def __init__( + self, + client: Client, + input: ListSchedulesInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_schedules`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[ScheduleListDescription] | None = None + self._current_page_index = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[ScheduleListDescription] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page if any. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + resp = await self._client.workflow_service.list_schedules( + temporalio.api.workflowservice.v1.ListSchedulesRequest( + namespace=self._client.namespace, + maximum_page_size=page_size or self._input.page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + self._current_page = [ + ScheduleListDescription._from_proto(v, self._client.data_converter) + for v in resp.schedules + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> ScheduleAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> ScheduleListDescription: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + return ret diff --git a/temporalio/client/_worker_versioning.py b/temporalio/client/_worker_versioning.py new file mode 100644 index 000000000..d6ef8f257 --- /dev/null +++ b/temporalio/client/_worker_versioning.py @@ -0,0 +1,278 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import ( + Mapping, + Sequence, +) +from dataclasses import dataclass +from enum import Enum + +import temporalio.api.enums.v1 +import temporalio.api.workflowservice.v1 + + +@dataclass(frozen=True) +class WorkerBuildIdVersionSets: + """Represents the sets of compatible Build ID versions associated with some Task Queue, as + fetched by :py:meth:`Client.get_worker_build_id_compatibility`. + """ + + version_sets: Sequence[BuildIdVersionSet] + """All version sets that were fetched for this task queue.""" + + def default_set(self) -> BuildIdVersionSet: + """Returns the default version set for this task queue.""" + return self.version_sets[-1] + + def default_build_id(self) -> str: + """Returns the default Build ID for this task queue.""" + return self.default_set().default() + + @staticmethod + def _from_proto( + resp: temporalio.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse, + ) -> WorkerBuildIdVersionSets: + return WorkerBuildIdVersionSets( + version_sets=[ + BuildIdVersionSet(mvs.build_ids) for mvs in resp.major_version_sets + ] + ) + + +@dataclass(frozen=True) +class BuildIdVersionSet: + """A set of Build IDs which are compatible with each other.""" + + build_ids: Sequence[str] + """All Build IDs contained in the set.""" + + def default(self) -> str: + """Returns the default Build ID for this set.""" + return self.build_ids[-1] + + +class BuildIdOp(ABC): + """Base class for Build ID operations as used by + :py:meth:`Client.update_worker_build_id_compatibility`. + """ + + @abstractmethod + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + """Returns a partial request with the operation populated. Caller must populate + non-operation fields. This is done b/c there's no good way to assign a non-primitive message + as the operation after initializing the request. + """ + ... + + +@dataclass(frozen=True) +class BuildIdOpAddNewDefault(BuildIdOp): + """Adds a new Build Id into a new set, which will be used as the default set for + the queue. This means all new workflows will start on this Build Id. + """ + + build_id: str + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return ( + temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + add_new_build_id_in_new_default_set=self.build_id + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpAddNewCompatible(BuildIdOp): + """Adds a new Build Id into an existing compatible set. The newly added ID becomes + the default for that compatible set, and thus new workflow tasks for workflows which have been + executing on workers in that set will now start on this new Build Id. + """ + + build_id: str + """The Build Id to add to the compatible set.""" + + existing_compatible_build_id: str + """A Build Id which must already be defined on the task queue, and is used to find the + compatible set to add the new id to. + """ + + promote_set: bool = False + """If set to true, the targeted set will also be promoted to become the overall default set for + the queue.""" + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + add_new_compatible_build_id=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion( + new_build_id=self.build_id, + existing_compatible_build_id=self.existing_compatible_build_id, + make_set_default=self.promote_set, + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpPromoteSetByBuildId(BuildIdOp): + """Promotes a set of compatible Build Ids to become the current default set for the task queue. + Any Build Id in the set may be used to target it. + """ + + build_id: str + """A Build Id which must already be defined on the task queue, and is used to find the + compatible set to promote.""" + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return ( + temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + promote_set_by_build_id=self.build_id + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpPromoteBuildIdWithinSet(BuildIdOp): + """Promotes a Build Id within an existing set to become the default ID for that set.""" + + build_id: str + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return ( + temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + promote_build_id_within_set=self.build_id + ) + ) + + +@dataclass(frozen=True) +class BuildIdOpMergeSets(BuildIdOp): + """Merges two sets into one set, thus declaring all the Build Ids in both as compatible with one + another. The default of the primary set is maintained as the merged set's overall default. + """ + + primary_build_id: str + """A Build Id which and is used to find the primary set to be merged.""" + + secondary_build_id: str + """A Build Id which and is used to find the secondary set to be merged.""" + + def _as_partial_proto( + self, + ) -> temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest: + return temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest( + merge_sets=temporalio.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.MergeSets( + primary_set_build_id=self.primary_build_id, + secondary_set_build_id=self.secondary_build_id, + ) + ) + + +@dataclass(frozen=True) +class WorkerTaskReachability: + """Contains information about the reachability of some Build IDs""" + + build_id_reachability: Mapping[str, BuildIdReachability] + """Maps Build IDs to information about their reachability""" + + @staticmethod + def _from_proto( + resp: temporalio.api.workflowservice.v1.GetWorkerTaskReachabilityResponse, + ) -> WorkerTaskReachability: + mapping = dict() + for bid_reach in resp.build_id_reachability: + tq_mapping = dict() + unretrieved = set() + for tq_reach in bid_reach.task_queue_reachability: + if tq_reach.reachability == [ + temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED + ]: + unretrieved.add(tq_reach.task_queue) + continue + tq_mapping[tq_reach.task_queue] = [ + TaskReachabilityType._from_proto(r) for r in tq_reach.reachability + ] + + mapping[bid_reach.build_id] = BuildIdReachability( + task_queue_reachability=tq_mapping, + unretrieved_task_queues=frozenset(unretrieved), + ) + + return WorkerTaskReachability(build_id_reachability=mapping) + + +@dataclass(frozen=True) +class BuildIdReachability: + """Contains information about the reachability of a specific Build ID""" + + task_queue_reachability: Mapping[str, Sequence[TaskReachabilityType]] + """Maps Task Queue names to the reachability status of the Build ID on that queue. If the value + is an empty list, the Build ID is not reachable on that queue. + """ + + unretrieved_task_queues: frozenset[str] + """If any Task Queues could not be retrieved because the server limits the number that can be + queried at once, they will be listed here. + """ + + +class TaskReachabilityType(Enum): + """Enumerates how a task might reach certain kinds of workflows""" + + NEW_WORKFLOWS = 1 + EXISTING_WORKFLOWS = 2 + OPEN_WORKFLOWS = 3 + CLOSED_WORKFLOWS = 4 + + @staticmethod + def _from_proto( + reachability: temporalio.api.enums.v1.TaskReachability.ValueType, + ) -> TaskReachabilityType: + if ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS + ): + return TaskReachabilityType.NEW_WORKFLOWS + elif ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS + ): + return TaskReachabilityType.EXISTING_WORKFLOWS + elif ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS + ): + return TaskReachabilityType.OPEN_WORKFLOWS + elif ( + reachability + == temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS + ): + return TaskReachabilityType.CLOSED_WORKFLOWS + else: + raise ValueError(f"Cannot convert reachability type: {reachability}") + + def _to_proto(self) -> temporalio.api.enums.v1.TaskReachability.ValueType: + if self == TaskReachabilityType.NEW_WORKFLOWS: + return ( + temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_NEW_WORKFLOWS + ) + elif self == TaskReachabilityType.EXISTING_WORKFLOWS: + return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_EXISTING_WORKFLOWS + elif self == TaskReachabilityType.OPEN_WORKFLOWS: + return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_OPEN_WORKFLOWS + elif self == TaskReachabilityType.CLOSED_WORKFLOWS: + return temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_CLOSED_WORKFLOWS + else: + return ( + temporalio.api.enums.v1.TaskReachability.TASK_REACHABILITY_UNSPECIFIED + ) diff --git a/temporalio/client/_workflow.py b/temporalio/client/_workflow.py new file mode 100644 index 000000000..22ac00d84 --- /dev/null +++ b/temporalio/client/_workflow.py @@ -0,0 +1,2000 @@ +"""Client support for accessing Temporal.""" + +from __future__ import annotations + +import asyncio +import functools +import warnings +from asyncio import Future +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Mapping, + Sequence, +) +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import IntEnum +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + Generic, + cast, + overload, +) + +import google.protobuf.json_format +from typing_extensions import Self + +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.history.v1 +import temporalio.api.update.v1 +import temporalio.api.workflow.v1 +import temporalio.api.workflowservice.v1 +import temporalio.common +import temporalio.converter +import temporalio.converter._search_attributes +import temporalio.exceptions +import temporalio.workflow +from temporalio.converter import ( + WorkflowSerializationContext, +) +from temporalio.service import ( + RPCError, + RPCStatusCode, +) + +from ..types import ( + AnyType, + LocalReturnType, + MethodAsyncNoParam, + MethodAsyncSingleParam, + MethodSyncOrAsyncNoParam, + MethodSyncOrAsyncSingleParam, + MultiParamSpec, + ParamType, + ReturnType, + SelfType, +) +from ._exceptions import ( + WorkflowContinuedAsNewError, + WorkflowFailureError, + WorkflowUpdateFailedError, + WorkflowUpdateRPCTimeoutOrCancelledError, +) +from ._helpers import _decode_user_metadata, _history_from_json +from ._interceptor import ( + CancelWorkflowInput, + DescribeWorkflowInput, + FetchWorkflowHistoryEventsInput, + QueryWorkflowInput, + SignalWorkflowInput, + StartWorkflowUpdateInput, + TerminateWorkflowInput, + UpdateWithStartStartWorkflowInput, +) + +if TYPE_CHECKING: + from ._client import Client + from ._interceptor import ListWorkflowsInput + + +class WorkflowHistoryEventFilterType(IntEnum): + """Type of history events to get for a workflow. + + See :py:class:`temporalio.api.enums.v1.HistoryEventFilterType`. + """ + + ALL_EVENT = int( + temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT + ) + CLOSE_EVENT = int( + temporalio.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT + ) + + +class WorkflowHandle(Generic[SelfType, ReturnType]): + """Handle for interacting with a workflow. + + This is usually created via :py:meth:`Client.get_workflow_handle` or + returned from :py:meth:`Client.start_workflow`. + """ + + def __init__( + self, + client: Client, + id: str, + *, + run_id: str | None = None, + result_run_id: str | None = None, + first_execution_run_id: str | None = None, + result_type: type | None = None, + start_workflow_response: None + | ( + temporalio.api.workflowservice.v1.StartWorkflowExecutionResponse + | temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse + ) = None, + ) -> None: + """Create workflow handle.""" + self._client = client + self._id = id + self._run_id = run_id + self._result_run_id = result_run_id + self._first_execution_run_id = first_execution_run_id + self._result_type = result_type + self._start_workflow_response = start_workflow_response + self.__temporal_eagerly_started = False + + @functools.cached_property + def _data_converter(self) -> temporalio.converter.DataConverter: + return self._client.data_converter.with_context( + temporalio.converter.WorkflowSerializationContext( + namespace=self._client.namespace, workflow_id=self._id + ) + ) + + @property + def id(self) -> str: + """ID of the workflow.""" + return self._id + + @property + def run_id(self) -> str | None: + """If present, run ID used to ensure that requested operations apply + to this exact run. + + This is only created via :py:meth:`Client.get_workflow_handle`. + :py:meth:`Client.start_workflow` will not set this value. + + This cannot be mutated. If a different run ID is needed, + :py:meth:`Client.get_workflow_handle` must be used instead. + """ + return self._run_id + + @property + def result_run_id(self) -> str | None: + """Run ID used for :py:meth:`result` calls if present to ensure result + is for a workflow starting from this run. + + When this handle is created via :py:meth:`Client.get_workflow_handle`, + this is the same as run_id. When this handle is created via + :py:meth:`Client.start_workflow`, this value will be the resulting run + ID. + + This cannot be mutated. If a different run ID is needed, + :py:meth:`Client.get_workflow_handle` must be used instead. + """ + return self._result_run_id + + @property + def first_execution_run_id(self) -> str | None: + """Run ID used to ensure requested operations apply to a workflow ID + started with this run ID. + + This can be set when using :py:meth:`Client.get_workflow_handle`. When + :py:meth:`Client.start_workflow` is called without a start signal, this + is set to the resulting run. + + This cannot be mutated. If a different first execution run ID is needed, + :py:meth:`Client.get_workflow_handle` must be used instead. + """ + return self._first_execution_run_id + + async def result( + self, + *, + follow_runs: bool = True, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> ReturnType: + """Wait for result of the workflow. + + This will use :py:attr:`result_run_id` if present to base the result on. + To use another run ID, a new handle must be created via + :py:meth:`Client.get_workflow_handle`. + + Args: + follow_runs: If true (default), workflow runs will be continually + fetched, until the most recent one is found. If false, return + the result from the first run targeted by the request if that run + ends in a result, otherwise raise an exception. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note, + this is the timeout for each history RPC call not this overall + function. + + Returns: + Result of the workflow after being converted by the data converter. + + Raises: + WorkflowFailureError: Workflow failed, was cancelled, was + terminated, or timed out. Use the + :py:attr:`WorkflowFailureError.cause` to see the underlying + reason. + Exception: Other possible failures during result fetching. + """ + # We have to maintain our own run ID because it can change if we follow + # executions + hist_run_id = self._result_run_id + while True: + async for event in self._fetch_history_events_for_run( + hist_run_id, + wait_new_event=True, + event_filter_type=WorkflowHistoryEventFilterType.CLOSE_EVENT, + skip_archival=True, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ): + if event.HasField("workflow_execution_completed_event_attributes"): + complete_attr = event.workflow_execution_completed_event_attributes + # Follow execution + if follow_runs and complete_attr.new_execution_run_id: + hist_run_id = complete_attr.new_execution_run_id + break + # Ignoring anything after the first response like TypeScript + type_hints = [self._result_type] if self._result_type else None + results = await self._data_converter.decode_wrapper( + complete_attr.result, + type_hints, + ) + if not results: + return cast(ReturnType, None) + elif len(results) > 1: + warnings.warn(f"Expected single result, got {len(results)}") + return cast(ReturnType, results[0]) + elif event.HasField("workflow_execution_failed_event_attributes"): + fail_attr = event.workflow_execution_failed_event_attributes + # Follow execution + if follow_runs and fail_attr.new_execution_run_id: + hist_run_id = fail_attr.new_execution_run_id + break + raise WorkflowFailureError( + cause=await self._data_converter.decode_failure( + fail_attr.failure + ), + ) + elif event.HasField("workflow_execution_canceled_event_attributes"): + cancel_attr = event.workflow_execution_canceled_event_attributes + raise WorkflowFailureError( + cause=temporalio.exceptions.CancelledError( + "Workflow cancelled", + *( + await self._data_converter.decode_wrapper( + cancel_attr.details + ) + ), + ) + ) + elif event.HasField("workflow_execution_terminated_event_attributes"): + term_attr = event.workflow_execution_terminated_event_attributes + raise WorkflowFailureError( + cause=temporalio.exceptions.TerminatedError( + term_attr.reason or "Workflow terminated", + *( + await self._data_converter.decode_wrapper( + term_attr.details + ) + ), + ), + ) + elif event.HasField("workflow_execution_timed_out_event_attributes"): + time_attr = event.workflow_execution_timed_out_event_attributes + # Follow execution + if follow_runs and time_attr.new_execution_run_id: + hist_run_id = time_attr.new_execution_run_id + break + raise WorkflowFailureError( + cause=temporalio.exceptions.TimeoutError( + "Workflow timed out", + type=temporalio.exceptions.TimeoutType.START_TO_CLOSE, + last_heartbeat_details=[], + ), + ) + elif event.HasField( + "workflow_execution_continued_as_new_event_attributes" + ): + cont_attr = ( + event.workflow_execution_continued_as_new_event_attributes + ) + if not cont_attr.new_execution_run_id: + raise RuntimeError( + "Unexpectedly missing new run ID from continue as new" + ) + # Follow execution + if follow_runs: + hist_run_id = cont_attr.new_execution_run_id + break + raise WorkflowContinuedAsNewError(cont_attr.new_execution_run_id) + # This is reached on break which means that there's a different run + # ID if we're following. If there's not, it's an error because no + # event was given (should never happen). + if hist_run_id is None: + raise RuntimeError("No completion event found") + + async def cancel( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Cancel the workflow. + + This will issue a cancellation for :py:attr:`run_id` if present. This + call will make sure to use the run chain starting from + :py:attr:`first_execution_run_id` if present. To create handles with + these values, use :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` with + a start signal will cancel the latest workflow with the same + workflow ID even if it is unrelated to the started workflow. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: Workflow could not be cancelled. + """ + await self._client._impl.cancel_workflow( + CancelWorkflowInput( + id=self._id, + run_id=self._run_id, + first_execution_run_id=self._first_execution_run_id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def describe( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowExecutionDescription: + """Get workflow details. + + This will get details for :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via + :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + describe the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Workflow details. + + Raises: + RPCError: Workflow details could not be fetched. + """ + return await self._client._impl.describe_workflow( + DescribeWorkflowInput( + id=self._id, + run_id=self._run_id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def fetch_history( + self, + *, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowHistory: + """Get workflow history. + + This is a shortcut for :py:meth:`fetch_history_events` that just fetches + all events. + """ + return WorkflowHistory( + workflow_id=self.id, + events=[ + v + async for v in self.fetch_history_events( + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ], + ) + + def fetch_history_events( + self, + *, + page_size: int | None = None, + next_page_token: bytes | None = None, + wait_new_event: bool = False, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowHistoryEventAsyncIterator: + """Get workflow history events as an async iterator. + + This does not make a request until the first iteration is attempted. + Therefore any errors will not occur until then. + + Args: + page_size: Maximum amount to fetch per request if any maximum. + next_page_token: A specific page token to fetch. + wait_new_event: Whether the event fetching request will wait for new + events or just return right away. + event_filter_type: Which events to obtain. + skip_archival: Whether to skip archival. + rpc_metadata: Headers used on each RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. + + Returns: + An async iterator that doesn't begin fetching until iterated on. + """ + return self._fetch_history_events_for_run( + self._run_id, + page_size=page_size, + next_page_token=next_page_token, + wait_new_event=wait_new_event, + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + def _fetch_history_events_for_run( + self, + run_id: str | None, + *, + page_size: int | None = None, + next_page_token: bytes | None = None, + wait_new_event: bool = False, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowHistoryEventAsyncIterator: + return self._client._impl.fetch_workflow_history_events( + FetchWorkflowHistoryEventsInput( + id=self._id, + run_id=run_id, + page_size=page_size, + next_page_token=next_page_token, + wait_new_event=wait_new_event, + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + # Overload for no-param query + @overload + async def query( + self, + query: MethodSyncOrAsyncNoParam[SelfType, LocalReturnType], + *, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for single-param query + @overload + async def query( + self, + query: MethodSyncOrAsyncSingleParam[SelfType, ParamType, LocalReturnType], + arg: ParamType, + *, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for multi-param query + @overload + async def query( + self, + query: Callable[ + Concatenate[SelfType, MultiParamSpec], + Awaitable[LocalReturnType] | LocalReturnType, + ], + *, + args: Sequence[Any], + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for string-name query + @overload + async def query( + self, + query: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def query( + self, + query: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + result_type: type | None = None, + reject_condition: temporalio.common.QueryRejectCondition | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Query the workflow. + + This will query for :py:attr:`run_id` if present. To use a different + run ID, create a new handle with + :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + query the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + query: Query function or name on the workflow. + arg: Single argument to the query. + args: Multiple arguments to the query. Cannot be set if arg is. + result_type: For string queries, this can set the specific result + type hint to deserialize into. + reject_condition: Condition for rejecting the query. If unset/None, + defaults to the client's default (which is defaulted to None). + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Returns: + Result of the query. + + Raises: + WorkflowQueryRejectedError: A query reject condition was satisfied. + RPCError: Workflow details could not be fetched. + """ + query_name: str + ret_type = result_type + if callable(query): + defn = temporalio.workflow._QueryDefinition.from_fn(query) + if not defn: + raise RuntimeError( + f"Query definition not found on {query.__qualname__}, " + "is it decorated with @workflow.query?" + ) + elif not defn.name: + raise RuntimeError("Cannot invoke dynamic query definition") + # TODO(cretz): Check count/type of args at runtime? + query_name = defn.name + ret_type = defn.ret_type + else: + query_name = str(query) + + return await self._client._impl.query_workflow( + QueryWorkflowInput( + id=self._id, + run_id=self._run_id, + query=query_name, + args=temporalio.common._arg_or_args(arg, args), + reject_condition=reject_condition + or self._client._config["default_workflow_query_reject_condition"], + headers={}, + ret_type=ret_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + # Overload for no-param signal + @overload + async def signal( + self, + signal: MethodSyncOrAsyncNoParam[SelfType, None], + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + # Overload for single-param signal + @overload + async def signal( + self, + signal: MethodSyncOrAsyncSingleParam[SelfType, ParamType, None], + arg: ParamType, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + # Overload for multi-param signal + @overload + async def signal( + self, + signal: Callable[Concatenate[SelfType, MultiParamSpec], Awaitable[None] | None], + *, + args: Sequence[Any], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + # Overload for string-name signal + @overload + async def signal( + self, + signal: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: ... + + async def signal( + self, + signal: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Send a signal to the workflow. + + This will signal for :py:attr:`run_id` if present. To use a different + run ID, create a new handle with via + :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` will + signal the latest workflow with the same workflow ID even if it is + unrelated to the started workflow. + + Args: + signal: Signal function or name on the workflow. + arg: Single argument to the signal. + args: Multiple arguments to the signal. Cannot be set if arg is. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: Workflow could not be signalled. + """ + await self._client._impl.signal_workflow( + SignalWorkflowInput( + id=self._id, + run_id=self._run_id, + signal=temporalio.workflow._SignalDefinition.must_name_from_fn_or_str( + signal + ), + args=temporalio.common._arg_or_args(arg, args), + headers={}, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + async def terminate( + self, + *args: Any, + reason: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + """Terminate the workflow. + + This will issue a termination for :py:attr:`run_id` if present. This + call will make sure to use the run chain starting from + :py:attr:`first_execution_run_id` if present. To create handles with + these values, use :py:meth:`Client.get_workflow_handle`. + + .. warning:: + Handles created as a result of :py:meth:`Client.start_workflow` with + a start signal will terminate the latest workflow with the same + workflow ID even if it is unrelated to the started workflow. + + Args: + args: Details to store on the termination. + reason: Reason for the termination. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + RPCError: Workflow could not be terminated. + """ + await self._client._impl.terminate_workflow( + TerminateWorkflowInput( + id=self._id, + run_id=self._run_id, + args=args, + reason=reason, + first_execution_run_id=self._first_execution_run_id, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + ) + + # Overload for no-param update + @overload + async def execute_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for single-param update + @overload + async def execute_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for multi-param update + @overload + async def execute_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: ... + + # Overload for string-name update + @overload + async def execute_update( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: ... + + async def execute_update( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> Any: + """Send an update request to the workflow and wait for it to complete. + + This will target the workflow with :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + + Args: + update: Update function or name on the workflow. + arg: Single argument to the update. + args: Multiple arguments to the update. Cannot be set if arg is. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed + out or cancelled. + RPCError: There was some issue sending the update to the workflow. + """ + handle = await self._start_update( + update, + arg, + args=args, + wait_for_stage=WorkflowUpdateStage.COMPLETED, + id=id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + return await handle.result() + + # Overload for no-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[[SelfType], LocalReturnType], + *, + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for single-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + [SelfType, ParamType], LocalReturnType + ], + arg: ParamType, + *, + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for multi-param start update + @overload + async def start_update( + self, + update: temporalio.workflow.UpdateMethodMultiParam[ + MultiParamSpec, LocalReturnType + ], + *, + args: MultiParamSpec.args, # type: ignore + wait_for_stage: WorkflowUpdateStage, + id: str | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: ... + + # Overload for string-name start update + @overload + async def start_update( + self, + update: str, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: ... + + async def start_update( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + """Send an update request to the workflow and return a handle to it. + + This will target the workflow with :py:attr:`run_id` if present. To use a + different run ID, create a new handle with via :py:meth:`Client.get_workflow_handle`. + + Args: + update: Update function or name on the workflow. arg: Single argument to the + update. + wait_for_stage: Required stage to wait until returning: either ACCEPTED or + COMPLETED. ADMITTED is not currently supported. See + https://docs.temporal.io/workflows#update for more details. + args: Multiple arguments to the update. Cannot be set if arg is. + id: ID of the update. If not set, the default is a new UUID. + result_type: For string updates, this can set the specific result + type hint to deserialize into. + rpc_metadata: Headers used on the RPC call. Keys here override + client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for the RPC call. + + Raises: + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed out or + cancelled. + RPCError: There was some issue sending the update to the workflow. + """ + return await self._start_update( + update, + arg, + wait_for_stage=wait_for_stage, + args=args, + id=id, + result_type=result_type, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + async def _start_update( + self, + update: str | Callable, + arg: Any = temporalio.common._arg_unset, + *, + wait_for_stage: WorkflowUpdateStage, + args: Sequence[Any] = [], + id: str | None = None, + result_type: type | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> WorkflowUpdateHandle[Any]: + if wait_for_stage == WorkflowUpdateStage.ADMITTED: + raise ValueError("ADMITTED wait stage not supported") + + update_name, result_type_from_type_hint = ( + temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) + ) + + return await self._client._impl.start_workflow_update( + StartWorkflowUpdateInput( + id=self._id, + run_id=self._run_id, + first_execution_run_id=self.first_execution_run_id, + update_id=id, + update=update_name, + args=temporalio.common._arg_or_args(arg, args), + headers={}, + ret_type=result_type or result_type_from_type_hint, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + wait_for_stage=wait_for_stage, + ) + ) + + def get_update_handle( + self, + id: str, + *, + workflow_run_id: str | None = None, + result_type: type | None = None, + ) -> WorkflowUpdateHandle[Any]: + """Get a handle for an update. The handle can be used to wait on the + update result. + + Users may prefer the more typesafe :py:meth:`get_update_handle_for` + which accepts an update definition. + + Args: + id: Update ID to get a handle to. + workflow_run_id: Run ID to tie the handle to. If this is not set, + the :py:attr:`run_id` will be used. + result_type: The result type to deserialize into if known. + + Returns: + The update handle. + """ + return WorkflowUpdateHandle( + self._client, + id, + self._id, + workflow_run_id=workflow_run_id or self._run_id, + result_type=result_type, + ) + + def get_update_handle_for( + self, + update: temporalio.workflow.UpdateMethodMultiParam[Any, LocalReturnType], + id: str, + *, + workflow_run_id: str | None = None, + ) -> WorkflowUpdateHandle[LocalReturnType]: + """Get a typed handle for an update. The handle can be used to wait on + the update result. + + This is the same as :py:meth:`get_update_handle` but typed. + + Args: + update: The update method to use for typing the handle. + id: Update ID to get a handle to. + workflow_run_id: Run ID to tie the handle to. If this is not set, + the :py:attr:`run_id` will be used. + + Returns: + The update handle. + """ + return self.get_update_handle( + id, workflow_run_id=workflow_run_id, result_type=update._defn.ret_type + ) + + +class WithStartWorkflowOperation(Generic[SelfType, ReturnType]): + """Defines a start-workflow operation used by update-with-start requests. + + Update-With-Start allows you to send an update to a workflow, while starting the + workflow if necessary. + """ + + # Overload for no-param workflow, with_start + @overload + def __init__( + self, + workflow: MethodAsyncNoParam[SelfType, ReturnType], + *, + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + # Overload for single-param workflow, with_start + @overload + def __init__( + self, + workflow: MethodAsyncSingleParam[SelfType, ParamType, ReturnType], + arg: ParamType, + *, + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + # Overload for multi-param workflow, with_start + @overload + def __init__( + self, + workflow: Callable[ + Concatenate[SelfType, MultiParamSpec], Awaitable[ReturnType] + ], + *, + args: Sequence[Any], + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + # Overload for string-name workflow, with_start + @overload + def __init__( + self, + workflow: str, + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + ) -> None: ... + + def __init__( + self, + workflow: str | Callable[..., Awaitable[Any]], + arg: Any = temporalio.common._arg_unset, + *, + args: Sequence[Any] = [], + id: str, + task_queue: str, + id_conflict_policy: temporalio.common.WorkflowIDConflictPolicy, + result_type: type | None = None, + execution_timeout: timedelta | None = None, + run_timeout: timedelta | None = None, + task_timeout: timedelta | None = None, + id_reuse_policy: temporalio.common.WorkflowIDReusePolicy = temporalio.common.WorkflowIDReusePolicy.ALLOW_DUPLICATE, + retry_policy: temporalio.common.RetryPolicy | None = None, + cron_schedule: str = "", + memo: Mapping[str, Any] | None = None, + search_attributes: None + | ( + temporalio.common.TypedSearchAttributes | temporalio.common.SearchAttributes + ) = None, + static_summary: str | None = None, + static_details: str | None = None, + start_delay: timedelta | None = None, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + priority: temporalio.common.Priority = temporalio.common.Priority.default, + versioning_override: temporalio.common.VersioningOverride | None = None, + stack_level: int = 2, + ) -> None: + """Create a WithStartWorkflowOperation. + + See :py:meth:`temporalio.client.Client.start_workflow` for documentation of the + arguments. + """ + temporalio.common._warn_on_deprecated_search_attributes( + search_attributes, stack_level=stack_level + ) + name, result_type_from_run_fn = ( + temporalio.workflow._Definition.get_name_and_result_type(workflow) + ) + if id_conflict_policy == temporalio.common.WorkflowIDConflictPolicy.UNSPECIFIED: + raise ValueError("WorkflowIDConflictPolicy is required") + + self._start_workflow_input = UpdateWithStartStartWorkflowInput( + workflow=name, + args=temporalio.common._arg_or_args(arg, args), + id=id, + task_queue=task_queue, + execution_timeout=execution_timeout, + run_timeout=run_timeout, + task_timeout=task_timeout, + id_reuse_policy=id_reuse_policy, + id_conflict_policy=id_conflict_policy, + retry_policy=retry_policy, + cron_schedule=cron_schedule, + memo=memo, + search_attributes=search_attributes, + static_summary=static_summary, + static_details=static_details, + start_delay=start_delay, + headers={}, + ret_type=result_type or result_type_from_run_fn, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + priority=priority, + versioning_override=versioning_override, + ) + self._workflow_handle: Future[WorkflowHandle[SelfType, ReturnType]] = Future() + self._used = False + + async def workflow_handle(self) -> WorkflowHandle[SelfType, ReturnType]: + """Wait until workflow is running and return a WorkflowHandle.""" + return await self._workflow_handle + + +@dataclass +class WorkflowExecution: + """Info for a single workflow execution run.""" + + close_time: datetime | None + """When the workflow was closed if closed.""" + + execution_time: datetime | None + """When this workflow run started or should start.""" + + history_length: int + """Number of events in the history.""" + + id: str + """ID for the workflow.""" + + namespace: str + """Namespace for the workflow.""" + + parent_id: str | None + """ID for the parent workflow if this was started as a child.""" + + parent_run_id: str | None + """Run ID for the parent workflow if this was started as a child.""" + + root_id: str | None + """ID for the root workflow.""" + + root_run_id: str | None + """Run ID for the root workflow.""" + + raw_info: temporalio.api.workflow.v1.WorkflowExecutionInfo + """Underlying protobuf info.""" + + run_id: str + """Run ID for this workflow run.""" + + search_attributes: temporalio.common.SearchAttributes + """Current set of search attributes if any. + + .. deprecated:: + Use :py:attr:`typed_search_attributes` instead. + """ + + start_time: datetime + """When the workflow was created.""" + + status: WorkflowExecutionStatus | None + """Status for the workflow.""" + + task_queue: str + """Task queue for the workflow.""" + + typed_search_attributes: temporalio.common.TypedSearchAttributes + """Current set of search attributes if any.""" + + workflow_type: str + """Type name for the workflow.""" + + _context_free_data_converter: temporalio.converter.DataConverter + + @property + def data_converter(self) -> temporalio.converter.DataConverter: + """Data converter for the workflow.""" + return self._context_free_data_converter.with_context( + WorkflowSerializationContext( + namespace=self.namespace, + workflow_id=self.id, + ) + ) + + @classmethod + def _from_raw_info( + cls, + info: temporalio.api.workflow.v1.WorkflowExecutionInfo, + namespace: str, + converter: temporalio.converter.DataConverter, + **additional_fields: Any, + ) -> Self: + return cls( + close_time=( + info.close_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("close_time") + else None + ), + execution_time=( + info.execution_time.ToDatetime().replace(tzinfo=timezone.utc) + if info.HasField("execution_time") + else None + ), + history_length=info.history_length, + id=info.execution.workflow_id, + namespace=namespace, + parent_id=( + info.parent_execution.workflow_id + if info.HasField("parent_execution") + else None + ), + parent_run_id=( + info.parent_execution.run_id + if info.HasField("parent_execution") + else None + ), + root_id=( + info.root_execution.workflow_id + if info.HasField("root_execution") + else None + ), + root_run_id=( + info.root_execution.run_id if info.HasField("root_execution") else None + ), + raw_info=info, + run_id=info.execution.run_id, + search_attributes=temporalio.converter.decode_search_attributes( + info.search_attributes + ), + start_time=info.start_time.ToDatetime().replace(tzinfo=timezone.utc), + status=WorkflowExecutionStatus(info.status) if info.status else None, + task_queue=info.task_queue, + typed_search_attributes=temporalio.converter.decode_typed_search_attributes( + info.search_attributes + ), + workflow_type=info.type.name, + _context_free_data_converter=converter, + **additional_fields, + ) + + async def memo(self) -> Mapping[str, Any]: + """Workflow's memo values, converted without type hints. + + Since type hints are not used, the default converted values will come + back. For example, if the memo was originally created with a dataclass, + the value will be a dict. To convert using proper type hints, use + :py:meth:`memo_value`. + + Returns: + Mapping of all memo keys and they values without type hints. + """ + return await self.data_converter._decode_memo(self.raw_info.memo) + + @overload + async def memo_value( + self, key: str, default: Any = temporalio.common._arg_unset + ) -> Any: ... + + @overload + async def memo_value( + self, key: str, *, type_hint: type[ParamType] + ) -> ParamType: ... + + @overload + async def memo_value( + self, key: str, default: AnyType, *, type_hint: type[ParamType] + ) -> AnyType | ParamType: ... + + async def memo_value( + self, + key: str, + default: Any = temporalio.common._arg_unset, + *, + type_hint: type | None = None, + ) -> Any: + """Memo value for the given key, optional default, and optional type + hint. + + Args: + key: Key to get memo value for. + default: Default to use if key is not present. If unset, a + :py:class:`KeyError` is raised when the key does not exist. + type_hint: type hint to use when converting. + + Returns: + Memo value, converted with the type hint if present. + + Raises: + KeyError: Key not present and default not set. + """ + return await self.data_converter._decode_memo_field( + self.raw_info.memo, key, default, type_hint + ) + + +@dataclass +class WorkflowExecutionDescription(WorkflowExecution): + """Description for a single workflow execution run.""" + + raw_description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse + """Underlying protobuf description.""" + + _static_summary: str | None = None + _static_details: str | None = None + _metadata_decoded: bool = False + + async def static_summary(self) -> str | None: + """Gets the single-line fixed summary for this workflow execution that may appear in + UI/CLI. This can be in single-line Temporal markdown format. + """ + if not self._metadata_decoded: + await self._decode_metadata() + return self._static_summary + + async def static_details(self) -> str | None: + """Gets the general fixed details for this workflow execution that may appear in UI/CLI. + This can be in Temporal markdown format and can span multiple lines. + """ + if not self._metadata_decoded: + await self._decode_metadata() + return self._static_details + + async def _decode_metadata(self) -> None: + """Internal method to decode metadata lazily.""" + self._static_summary, self._static_details = await _decode_user_metadata( + self.data_converter, self.raw_description.execution_config.user_metadata + ) + self._metadata_decoded = True + + @staticmethod + async def _from_raw_description( + description: temporalio.api.workflowservice.v1.DescribeWorkflowExecutionResponse, + namespace: str, + converter: temporalio.converter.DataConverter, + ) -> WorkflowExecutionDescription: + return WorkflowExecutionDescription._from_raw_info( + description.workflow_execution_info, + namespace=namespace, + converter=converter, + raw_description=description, + ) + + +class WorkflowExecutionStatus(IntEnum): + """Status of a workflow execution. + + See :py:class:`temporalio.api.enums.v1.WorkflowExecutionStatus`. + """ + + RUNNING = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING + ) + COMPLETED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED + ) + FAILED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED + ) + CANCELED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED + ) + TERMINATED = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED + ) + CONTINUED_AS_NEW = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW + ) + TIMED_OUT = int( + temporalio.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT + ) + + +@dataclass +class WorkflowExecutionCount: + """Representation of a count from a count workflows call.""" + + count: int + """Approximate number of workflows matching the original query. + + If the query had a group-by clause, this is simply the sum of all the counts + in py:attr:`groups`. + """ + + groups: Sequence[WorkflowExecutionCountAggregationGroup] + """Groups if the query had a group-by clause, or empty if not.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse, + ) -> WorkflowExecutionCount: + return WorkflowExecutionCount( + count=raw.count, + groups=[ + WorkflowExecutionCountAggregationGroup._from_raw(g) for g in raw.groups + ], + ) + + +@dataclass +class WorkflowExecutionCountAggregationGroup: + """Aggregation group if the workflow count query had a group-by clause.""" + + count: int + """Approximate number of workflows matching the original query for this + group. + """ + + group_values: Sequence[temporalio.common.SearchAttributeValue] + """Search attribute values for this group.""" + + @staticmethod + def _from_raw( + raw: temporalio.api.workflowservice.v1.CountWorkflowExecutionsResponse.AggregationGroup, + ) -> WorkflowExecutionCountAggregationGroup: + return WorkflowExecutionCountAggregationGroup( + count=raw.count, + group_values=[ + temporalio.converter._search_attributes._decode_search_attribute_value( + v + ) + for v in raw.group_values + ], + ) + + +class WorkflowExecutionAsyncIterator: + """Asynchronous iterator for :py:class:`WorkflowExecution` values. + + Most users should use ``async for`` on this iterator and not call any of the + methods within. To consume the workflows as histories, call + :py:meth:`map_histories`. + """ + + def __init__( + self, + client: Client, + input: ListWorkflowsInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`Client.list_workflows`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: Sequence[WorkflowExecution] | None = None + self._current_page_index = 0 + self._limit = input.limit + self._yielded = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page(self) -> Sequence[WorkflowExecution] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: + """Fetch the next page if any. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + page_size = page_size or self._input.page_size + if self._limit is not None and self._limit - self._yielded < page_size: + page_size = self._limit - self._yielded + + resp = await self._client.workflow_service.list_workflow_executions( + temporalio.api.workflowservice.v1.ListWorkflowExecutionsRequest( + namespace=self._client.namespace, + page_size=page_size, + next_page_token=self._next_page_token or b"", + query=self._input.query or "", + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + + self._current_page = [ + WorkflowExecution._from_raw_info( + v, self._client.namespace, self._client.data_converter + ) + for v in resp.executions + ] + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> WorkflowExecutionAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> WorkflowExecution: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + if self._limit is not None and self._yielded >= self._limit: + raise StopAsyncIteration + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Get current, increment page index, and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + self._yielded += 1 + return ret + + async def map_histories( + self, + *, + event_filter_type: WorkflowHistoryEventFilterType = WorkflowHistoryEventFilterType.ALL_EVENT, + skip_archival: bool = False, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> AsyncIterator[WorkflowHistory]: + """Create an async iterator consuming all workflows and calling + :py:meth:`WorkflowHandle.fetch_history` on each one. + + This is just a shortcut for ``fetch_history``, see that method for + parameter details. + """ + async for v in self: + yield await self._client.get_workflow_handle( + v.id, run_id=v.run_id + ).fetch_history( + event_filter_type=event_filter_type, + skip_archival=skip_archival, + rpc_metadata=rpc_metadata, + rpc_timeout=rpc_timeout, + ) + + +@dataclass(frozen=True) +class WorkflowHistory: + """A workflow's ID and immutable history.""" + + workflow_id: str + """ID of the workflow.""" + + events: Sequence[temporalio.api.history.v1.HistoryEvent] + """History events for the workflow.""" + + @property + def run_id(self) -> str: + """Run ID extracted from the first event.""" + if not self.events: + raise RuntimeError("No events") + if not self.events[0].HasField("workflow_execution_started_event_attributes"): + raise RuntimeError("First event is not workflow start") + return self.events[ + 0 + ].workflow_execution_started_event_attributes.original_execution_run_id + + @staticmethod + def from_json(workflow_id: str, history: str | dict[str, Any]) -> WorkflowHistory: + """Construct a WorkflowHistory from an ID and a json dump of history. + + This is built to work both with Temporal UI/CLI JSON as well as + :py:meth:`to_json` even though they are slightly different. + + Args: + workflow_id: The workflow's ID + history: A string or parsed-to-dict representation of workflow + history + + Returns: + Workflow history + """ + parsed = _history_from_json(history) + return WorkflowHistory(workflow_id, parsed.events) + + def to_json(self) -> str: + """Convert this history to JSON. + + Note, this does not include the workflow ID. + """ + return google.protobuf.json_format.MessageToJson( + temporalio.api.history.v1.History(events=self.events) + ) + + def to_json_dict(self) -> dict[str, Any]: + """Convert this history to JSON-compatible dict. + + Note, this does not include the workflow ID. + """ + return google.protobuf.json_format.MessageToDict( + temporalio.api.history.v1.History(events=self.events) + ) + + +@dataclass +class WorkflowHistoryEventAsyncIterator: + """Asynchronous iterator for history events of a workflow. + + Most users should use ``async for`` on this iterator and not call any of the + methods within. + """ + + def __init__( + self, + client: Client, + input: FetchWorkflowHistoryEventsInput, + ) -> None: + """Create an asynchronous iterator for the given input. + + Users should not create this directly, but rather use + :py:meth:`WorkflowHandle.fetch_history_events`. + """ + self._client = client + self._input = input + self._next_page_token = input.next_page_token + self._current_page: ( + None | (Sequence[temporalio.api.history.v1.HistoryEvent]) + ) = None + self._current_page_index = 0 + + @property + def current_page_index(self) -> int: + """Index of the entry in the current page that will be returned from + the next :py:meth:`__anext__` call. + """ + return self._current_page_index + + @property + def current_page( + self, + ) -> Sequence[temporalio.api.history.v1.HistoryEvent] | None: + """Current page, if it has been fetched yet.""" + return self._current_page + + @property + def next_page_token(self) -> bytes | None: + """Token for the next page request if any.""" + return self._next_page_token + + async def fetch_next_page(self, *, page_size: int | None = None) -> None: # type:ignore[reportUnusedParameter] # https://github.com/temporalio/sdk-python/issues/1239 + """Fetch the next page if any. + + Args: + page_size: Override the page size this iterator was originally + created with. + """ + resp = await self._client.workflow_service.get_workflow_execution_history( + temporalio.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest( + namespace=self._client.namespace, + execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=self._input.id, + run_id=self._input.run_id or "", + ), + maximum_page_size=page_size or self._input.page_size or 0, + next_page_token=self._next_page_token or b"", + wait_new_event=self._input.wait_new_event, + history_event_filter_type=temporalio.api.enums.v1.HistoryEventFilterType.ValueType( + self._input.event_filter_type + ), + skip_archival=self._input.skip_archival, + ), + retry=True, + metadata=self._input.rpc_metadata, + timeout=self._input.rpc_timeout, + ) + # We don't support raw history + assert len(resp.raw_history) == 0 + self._current_page = list(resp.history.events) + self._current_page_index = 0 + self._next_page_token = resp.next_page_token or None + + def __aiter__(self) -> WorkflowHistoryEventAsyncIterator: + """Return self as the iterator.""" + return self + + async def __anext__(self) -> temporalio.api.history.v1.HistoryEvent: + """Get the next execution on this iterator, fetching next page if + necessary. + """ + while True: + # No page? fetch and continue + if self._current_page is None: + await self.fetch_next_page() + continue + # No more left in page? + if self._current_page_index >= len(self._current_page): + # If there is a next page token, try to get another page and try + # again + if self._next_page_token is not None: + await self.fetch_next_page() + continue + # No more pages means we're done + raise StopAsyncIteration + # Increment page index and return + ret = self._current_page[self._current_page_index] + self._current_page_index += 1 + return ret + + +class WorkflowUpdateHandle(Generic[LocalReturnType]): + """Handle for a workflow update execution request.""" + + def __init__( + self, + client: Client, + id: str, + workflow_id: str, + *, + workflow_run_id: str | None = None, + result_type: type | None = None, + known_outcome: temporalio.api.update.v1.Outcome | None = None, + ): + """Create a workflow update handle. + + Users should not create this directly, but rather use + :py:meth:`WorkflowHandle.start_update` or :py:meth:`WorkflowHandle.get_update_handle`. + """ + self._client = client + self._id = id + self._workflow_id = workflow_id + self._workflow_run_id = workflow_run_id + self._result_type = result_type + self._known_outcome = known_outcome + + @functools.cached_property + def _data_converter(self) -> temporalio.converter.DataConverter: + return self._client.data_converter.with_context( + WorkflowSerializationContext( + namespace=self._client.namespace, + workflow_id=self.workflow_id, + ) + ) + + @property + def id(self) -> str: + """ID of this Update request.""" + return self._id + + @property + def workflow_id(self) -> str: + """The ID of the Workflow targeted by this Update.""" + return self._workflow_id + + @property + def workflow_run_id(self) -> str | None: + """If specified, the specific run of the Workflow targeted by this Update.""" + return self._workflow_run_id + + async def result( + self, + *, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> LocalReturnType: + """Wait for and return the result of the update. The result may already be known in which case no network call + is made. Otherwise the result will be polled for until it is returned. + + Args: + rpc_metadata: Headers used on the RPC call. Keys here override client-level RPC metadata keys. + rpc_timeout: Optional RPC deadline to set for each RPC call. Note: this is the timeout for each + RPC call while polling, not a timeout for the function as a whole. If an individual RPC times out, + it will be retried until the result is available. + + Raises: + WorkflowUpdateFailedError: If the update failed. + WorkflowUpdateRPCTimeoutOrCancelledError: This update call timed out + or was cancelled. This doesn't mean the update itself was timed + out or cancelled. + RPCError: Update result could not be fetched for some other reason. + """ + # Poll until outcome reached + await self._poll_until_outcome( + rpc_metadata=rpc_metadata, rpc_timeout=rpc_timeout + ) + + # Convert outcome to failure or value + assert self._known_outcome + if self._known_outcome.HasField("failure"): + raise WorkflowUpdateFailedError( + await self._data_converter.decode_failure(self._known_outcome.failure), + ) + if not self._known_outcome.success.payloads: + return None # type: ignore + type_hints = [self._result_type] if self._result_type else None + results = await self._data_converter.decode( + self._known_outcome.success.payloads, type_hints + ) + if not results: + return None # type: ignore + elif len(results) > 1: + warnings.warn(f"Expected single update result, got {len(results)}") + return results[0] + + async def _poll_until_outcome( + self, + rpc_metadata: Mapping[str, str | bytes] = {}, + rpc_timeout: timedelta | None = None, + ) -> None: + if self._known_outcome: + return + req = temporalio.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest( + namespace=self._client.namespace, + update_ref=temporalio.api.update.v1.UpdateRef( + workflow_execution=temporalio.api.common.v1.WorkflowExecution( + workflow_id=self.workflow_id, + run_id=self.workflow_run_id or "", + ), + update_id=self.id, + ), + identity=self._client.identity, + wait_policy=temporalio.api.update.v1.WaitPolicy( + lifecycle_stage=temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED + ), + ) + + # Continue polling as long as we have no outcome + while True: + try: + res = ( + await self._client.workflow_service.poll_workflow_execution_update( + req, + retry=True, + metadata=rpc_metadata, + timeout=rpc_timeout, + ) + ) + if res.HasField("outcome"): + self._known_outcome = res.outcome + return + except RPCError as err: + if ( + err.status == RPCStatusCode.DEADLINE_EXCEEDED + or err.status == RPCStatusCode.CANCELLED + ): + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + else: + raise + except asyncio.CancelledError as err: + raise WorkflowUpdateRPCTimeoutOrCancelledError() from err + + +class WorkflowUpdateStage(IntEnum): + """Stage to wait for workflow update to reach before returning from + ``start_update``. + """ + + ADMITTED = int( + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED + ) + ACCEPTED = int( + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED + ) + COMPLETED = int( + temporalio.api.enums.v1.UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED + ) diff --git a/tests/test_client_exports.py b/tests/test_client_exports.py new file mode 100644 index 000000000..6f4a6eb04 --- /dev/null +++ b/tests/test_client_exports.py @@ -0,0 +1,216 @@ +import temporalio.client + +# Generated from temporalio.client on main +EXPECTED_CLIENT_EXPORTS = [ + "ActivityCancellationDetails", + "ActivityExecution", + "ActivityExecutionAsyncIterator", + "ActivityExecutionCount", + "ActivityExecutionCountAggregationGroup", + "ActivityExecutionDescription", + "ActivityExecutionStatus", + "ActivityFailureError", + "ActivityHandle", + "ActivitySerializationContext", + "AnyType", + "AsyncActivityCancelledError", + "AsyncActivityHandle", + "AsyncActivityIDReference", + "BackfillScheduleInput", + "BuildIdOp", + "BuildIdOpAddNewCompatible", + "BuildIdOpAddNewDefault", + "BuildIdOpMergeSets", + "BuildIdOpPromoteBuildIdWithinSet", + "BuildIdOpPromoteSetByBuildId", + "BuildIdReachability", + "BuildIdVersionSet", + "CallableAsyncNoParam", + "CallableAsyncSingleParam", + "CallableSyncNoParam", + "CallableSyncSingleParam", + "Callback", + "CancelActivityInput", + "CancelWorkflowInput", + "Client", + "ClientConfig", + "ClientConnectConfig", + "CloudOperationsClient", + "CompleteAsyncActivityInput", + "ConnectConfig", + "CountActivitiesInput", + "CountWorkflowsInput", + "CreateScheduleInput", + "DataConverter", + "DeleteScheduleInput", + "DescribeActivityInput", + "DescribeScheduleInput", + "DescribeWorkflowInput", + "DnsLoadBalancingConfig", + "FailAsyncActivityInput", + "FetchWorkflowHistoryEventsInput", + "GetWorkerBuildIdCompatibilityInput", + "GetWorkerTaskReachabilityInput", + "HeaderCodecBehavior", + "HeartbeatAsyncActivityInput", + "HttpConnectProxyConfig", + "Interceptor", + "KeepAliveConfig", + "ListActivitiesInput", + "ListSchedulesInput", + "ListWorkflowsInput", + "LocalReturnType", + "MethodAsyncNoParam", + "MethodAsyncSingleParam", + "MethodSyncOrAsyncNoParam", + "MethodSyncOrAsyncSingleParam", + "MultiParamSpec", + "OutboundInterceptor", + "ParamType", + "PauseScheduleInput", + "PendingActivityState", + "Plugin", + "QueryWorkflowInput", + "RPCError", + "RPCStatusCode", + "RPCTimeoutOrCancelledError", + "ReportCancellationAsyncActivityInput", + "RetryConfig", + "ReturnType", + "Schedule", + "ScheduleAction", + "ScheduleActionExecution", + "ScheduleActionExecutionStartWorkflow", + "ScheduleActionResult", + "ScheduleActionStartWorkflow", + "ScheduleAlreadyRunningError", + "ScheduleAsyncIterator", + "ScheduleBackfill", + "ScheduleCalendarSpec", + "ScheduleDescription", + "ScheduleHandle", + "ScheduleInfo", + "ScheduleIntervalSpec", + "ScheduleListAction", + "ScheduleListActionStartWorkflow", + "ScheduleListDescription", + "ScheduleListInfo", + "ScheduleListSchedule", + "ScheduleListState", + "ScheduleOverlapPolicy", + "SchedulePolicy", + "ScheduleRange", + "ScheduleSpec", + "ScheduleState", + "ScheduleUpdate", + "ScheduleUpdateInput", + "SelfType", + "SerializationContext", + "ServiceClient", + "SignalWorkflowInput", + "StartActivityInput", + "StartWorkflowInput", + "StartWorkflowUpdateInput", + "StartWorkflowUpdateWithStartInput", + "StorageDriverActivityInfo", + "StorageDriverStoreContext", + "StorageDriverWorkflowInfo", + "TLSConfig", + "TaskReachabilityType", + "TerminateActivityInput", + "TerminateWorkflowInput", + "TriggerScheduleInput", + "UnpauseScheduleInput", + "UpdateScheduleInput", + "UpdateWithStartStartWorkflowInput", + "UpdateWithStartUpdateWorkflowInput", + "UpdateWorkerBuildIdCompatibilityInput", + "WithSerializationContext", + "WithStartWorkflowOperation", + "WorkerBuildIdVersionSets", + "WorkerTaskReachability", + "WorkflowContinuedAsNewError", + "WorkflowExecution", + "WorkflowExecutionAsyncIterator", + "WorkflowExecutionCount", + "WorkflowExecutionCountAggregationGroup", + "WorkflowExecutionDescription", + "WorkflowExecutionStatus", + "WorkflowFailureError", + "WorkflowHandle", + "WorkflowHistory", + "WorkflowHistoryEventAsyncIterator", + "WorkflowHistoryEventFilterType", + "WorkflowQueryFailedError", + "WorkflowQueryRejectedError", + "WorkflowSerializationContext", + "WorkflowUpdateFailedError", + "WorkflowUpdateHandle", + "WorkflowUpdateRPCTimeoutOrCancelledError", + "WorkflowUpdateStage", + "_ClientImpl", + "_apply_headers", + "_decode_user_metadata", + "_encode_user_metadata", + "_fix_history_enum", + "_fix_history_failure", + "_history_from_json", + "_pascal_case_match", + "annotations", +] + + +EXPECTED_INTENTIONALLY_REMOVED_CLIENT_EXPORTS = [ + "ABC", + "Any", + "AsyncIterator", + "Awaitable", + "Callable", + "Concatenate", + "Enum", + "Future", + "Generic", + "IntEnum", + "Iterable", + "Mapping", + "MessageMap", + "Required", + "Self", + "Sequence", + "TypedDict", + "abc", + "abstractmethod", + "asyncio", + "cast", + "copy", + "dataclass", + "dataclasses", + "datetime", + "functools", + "google", + "inspect", + "json", + "overload", + "re", + "temporalio", + "timedelta", + "timezone", + "uuid", + "warnings", +] + + +def test_client_module_exports_match_main() -> None: + missing = [ + name for name in EXPECTED_CLIENT_EXPORTS if not hasattr(temporalio.client, name) + ] + assert not missing + + +def test_client_module_drops_intentionally_removed_import_exports() -> None: + exported = [ + name + for name in EXPECTED_INTENTIONALLY_REMOVED_CLIENT_EXPORTS + if hasattr(temporalio.client, name) + ] + assert not exported