From 8a34d995b88616ef8d99f35d9e548eede534d41c Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 13 Jul 2025 22:07:12 -0400 Subject: [PATCH] Fix circular import: do not import temporalio.client in temporalio.nexus --- temporalio/client.py | 18 ++---------------- temporalio/nexus/__init__.py | 1 + temporalio/nexus/_decorators.py | 14 ++++---------- temporalio/nexus/_link_conversion.py | 5 ++++- temporalio/nexus/_operation_context.py | 22 ++++++++++++++++++++-- temporalio/nexus/_operation_handlers.py | 14 ++++++-------- temporalio/nexus/_token.py | 15 +++++++++------ 7 files changed, 46 insertions(+), 43 deletions(-) diff --git a/temporalio/client.py b/temporalio/client.py index b4b5453d7..44ab9570a 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -56,6 +56,7 @@ import temporalio.common import temporalio.converter import temporalio.exceptions +import temporalio.nexus import temporalio.runtime import temporalio.service import temporalio.workflow @@ -7321,23 +7322,8 @@ def api_key(self, value: Optional[str]) -> None: self.service_client.update_api_key(value) -@dataclass(frozen=True) -class NexusCallback: - """Nexus callback to attach to events such as workflow completion. - - .. warning:: - This API is experimental and unstable. - """ - - url: str - """Callback URL.""" - - headers: Mapping[str, str] - """Header to attach to callback request.""" - - # Intended to become a union of callback types -Callback = NexusCallback +Callback = temporalio.nexus.NexusCallback async def _encode_user_metadata( diff --git a/temporalio/nexus/__init__.py b/temporalio/nexus/__init__.py index c8bd1e40d..de9164716 100644 --- a/temporalio/nexus/__init__.py +++ b/temporalio/nexus/__init__.py @@ -9,6 +9,7 @@ from ._decorators import workflow_run_operation as workflow_run_operation from ._operation_context import Info as Info from ._operation_context import LoggerAdapter as LoggerAdapter +from ._operation_context import NexusCallback as NexusCallback from ._operation_context import ( WorkflowRunOperationContext as WorkflowRunOperationContext, ) diff --git a/temporalio/nexus/_decorators.py b/temporalio/nexus/_decorators.py index d8675afdb..3ea05f716 100644 --- a/temporalio/nexus/_decorators.py +++ b/temporalio/nexus/_decorators.py @@ -16,16 +16,10 @@ StartOperationContext, ) -from temporalio.nexus._operation_context import ( - WorkflowRunOperationContext, -) -from temporalio.nexus._operation_handlers import ( - WorkflowRunOperationHandler, -) -from temporalio.nexus._token import ( - WorkflowHandle, -) -from temporalio.nexus._util import ( +from ._operation_context import WorkflowRunOperationContext +from ._operation_handlers import WorkflowRunOperationHandler +from ._token import WorkflowHandle +from ._util import ( get_callable_name, get_workflow_run_start_method_input_and_output_type_annotations, set_operation_factory, diff --git a/temporalio/nexus/_link_conversion.py b/temporalio/nexus/_link_conversion.py index 9df56b9bf..a13c2d149 100644 --- a/temporalio/nexus/_link_conversion.py +++ b/temporalio/nexus/_link_conversion.py @@ -4,6 +4,7 @@ import re import urllib.parse from typing import ( + TYPE_CHECKING, Any, Optional, ) @@ -12,7 +13,9 @@ import temporalio.api.common.v1 import temporalio.api.enums.v1 -import temporalio.client + +if TYPE_CHECKING: + import temporalio.client logger = logging.getLogger(__name__) diff --git a/temporalio/nexus/_operation_context.py b/temporalio/nexus/_operation_context.py index f40ed460d..4439614a3 100644 --- a/temporalio/nexus/_operation_context.py +++ b/temporalio/nexus/_operation_context.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from datetime import timedelta from typing import ( + TYPE_CHECKING, Any, Callable, Optional, @@ -19,7 +20,6 @@ import temporalio.api.common.v1 import temporalio.api.workflowservice.v1 -import temporalio.client import temporalio.common from temporalio.nexus import _link_conversion from temporalio.nexus._token import WorkflowHandle @@ -32,6 +32,9 @@ SelfType, ) +if TYPE_CHECKING: + import temporalio.client + # The Temporal Nexus worker always builds a nexusrpc StartOperationContext or # CancelOperationContext and passes it as the first parameter to the nexusrpc operation # handler. In addition, it sets one of the following context vars. @@ -122,7 +125,7 @@ def _get_callbacks( ctx = self.nexus_context return ( [ - temporalio.client.NexusCallback( + NexusCallback( url=ctx.callback_url, headers=ctx.callback_headers, ) @@ -449,6 +452,21 @@ async def start_workflow( return WorkflowHandle[ReturnType]._unsafe_from_client_workflow_handle(wf_handle) +@dataclass(frozen=True) +class NexusCallback: + """Nexus callback to attach to events such as workflow completion. + + .. warning:: + This API is experimental and unstable. + """ + + url: str + """Callback URL.""" + + headers: Mapping[str, str] + """Header to attach to callback request.""" + + @dataclass(frozen=True) class _TemporalCancelOperationContext: """Context for a Nexus cancel operation being handled by a Temporal Nexus Worker.""" diff --git a/temporalio/nexus/_operation_handlers.py b/temporalio/nexus/_operation_handlers.py index 99b9ed101..cdfb81dd5 100644 --- a/temporalio/nexus/_operation_handlers.py +++ b/temporalio/nexus/_operation_handlers.py @@ -24,7 +24,6 @@ StartOperationResultAsync, ) -from temporalio import client from temporalio.nexus._operation_context import ( _temporal_cancel_operation_context, ) @@ -73,15 +72,14 @@ async def start( """Start the operation, by starting a workflow and completing asynchronously.""" handle = await self._start(ctx, input) if not isinstance(handle, WorkflowHandle): - if isinstance(handle, client.WorkflowHandle): - raise RuntimeError( - f"Expected {handle} to be a nexus.WorkflowHandle, but got a client.WorkflowHandle. " - f"You must use WorkflowRunOperationContext.start_workflow " - "to start a workflow that will deliver the result of the Nexus operation, " - "not client.Client.start_workflow." - ) raise RuntimeError( f"Expected {handle} to be a nexus.WorkflowHandle, but got {type(handle)}. " + f"When using @workflow_run_operation you must use " + "WorkflowRunOperationContext.start_workflow() " + "to start a workflow that will deliver the result of the Nexus operation, " + "and you must return the nexus.WorkflowHandle that it returns. " + "It is not possible to use client.Client.start_workflow() and client.WorkflowHandle " + "for this purpose." ) return StartOperationResultAsync(handle.to_token()) diff --git a/temporalio/nexus/_token.py b/temporalio/nexus/_token.py index 9793583a3..999e33767 100644 --- a/temporalio/nexus/_token.py +++ b/temporalio/nexus/_token.py @@ -3,15 +3,16 @@ import base64 import json from dataclasses import dataclass -from typing import Any, Generic, Literal, Optional, Type +from typing import TYPE_CHECKING, Any, Generic, Literal, Optional from nexusrpc import OutputT -from temporalio import client - OperationTokenType = Literal[1] OPERATION_TOKEN_TYPE_WORKFLOW: OperationTokenType = 1 +if TYPE_CHECKING: + import temporalio.client + @dataclass(frozen=True) class WorkflowHandle(Generic[OutputT]): @@ -32,8 +33,10 @@ class WorkflowHandle(Generic[OutputT]): version: Optional[int] = None def _to_client_workflow_handle( - self, client: client.Client, result_type: Optional[Type[OutputT]] = None - ) -> client.WorkflowHandle[Any, OutputT]: + self, + client: temporalio.client.Client, + result_type: Optional[type[OutputT]] = None, + ) -> temporalio.client.WorkflowHandle[Any, OutputT]: """Create a :py:class:`temporalio.client.WorkflowHandle` from the token.""" if client.namespace != self.namespace: raise ValueError( @@ -46,7 +49,7 @@ def _to_client_workflow_handle( # handle type. @classmethod def _unsafe_from_client_workflow_handle( - cls, workflow_handle: client.WorkflowHandle[Any, OutputT] + cls, workflow_handle: temporalio.client.WorkflowHandle[Any, OutputT] ) -> WorkflowHandle[OutputT]: """Create a :py:class:`WorkflowHandle` from a :py:class:`temporalio.client.WorkflowHandle`.