From 266dd3c7608c6db82241479c638e5c358c952366 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 20 Feb 2025 15:26:51 -0500 Subject: [PATCH 01/46] Nexus samples --- hello_nexus/README.md | 15 ++ hello_nexus/__init__.py | 0 hello_nexus/basic/README.md | 37 +++++ hello_nexus/basic/__init__.py | 0 hello_nexus/basic/caller/__init__.py | 1 + hello_nexus/basic/caller/app.py | 46 ++++++ hello_nexus/basic/caller/workflows.py | 35 ++++ hello_nexus/basic/handler/__init__.py | 0 hello_nexus/basic/handler/db_client.py | 23 +++ hello_nexus/basic/handler/service_handler.py | 69 ++++++++ ..._handler_with_operation_handler_classes.py | 155 ++++++++++++++++++ hello_nexus/basic/handler/worker.py | 68 ++++++++ hello_nexus/basic/handler/workflows.py | 14 ++ hello_nexus/basic/service.py | 33 ++++ hello_nexus/basic/service_description.md | 3 + .../without_service_definition/README.md | 25 +++ .../without_service_definition/__init__.py | 1 + hello_nexus/without_service_definition/app.py | 104 ++++++++++++ open_telemetry/worker.py | 2 +- pyproject.toml | 13 +- tests/hello_nexus/basic_test.py | 45 +++++ tests/hello_nexus/helpers.py | 41 +++++ .../without_workflow_service_test.py | 28 ++++ 23 files changed, 756 insertions(+), 2 deletions(-) create mode 100644 hello_nexus/README.md create mode 100644 hello_nexus/__init__.py create mode 100644 hello_nexus/basic/README.md create mode 100644 hello_nexus/basic/__init__.py create mode 100644 hello_nexus/basic/caller/__init__.py create mode 100644 hello_nexus/basic/caller/app.py create mode 100644 hello_nexus/basic/caller/workflows.py create mode 100644 hello_nexus/basic/handler/__init__.py create mode 100644 hello_nexus/basic/handler/db_client.py create mode 100644 hello_nexus/basic/handler/service_handler.py create mode 100644 hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py create mode 100644 hello_nexus/basic/handler/worker.py create mode 100644 hello_nexus/basic/handler/workflows.py create mode 100644 hello_nexus/basic/service.py create mode 100644 hello_nexus/basic/service_description.md create mode 100644 hello_nexus/without_service_definition/README.md create mode 100644 hello_nexus/without_service_definition/__init__.py create mode 100644 hello_nexus/without_service_definition/app.py create mode 100644 tests/hello_nexus/basic_test.py create mode 100644 tests/hello_nexus/helpers.py create mode 100644 tests/hello_nexus/without_workflow_service_test.py diff --git a/hello_nexus/README.md b/hello_nexus/README.md new file mode 100644 index 00000000..dafd1205 --- /dev/null +++ b/hello_nexus/README.md @@ -0,0 +1,15 @@ +# Nexus + +Temporal Nexus is a feature of the Temporal platform designed to connect durable executions across team, namespace, +region, and cloud boundaries. It promotes a more modular architecture for sharing a subset of your team’s capabilities +via well-defined service API contracts for other teams to use. These can abstract underlying Temporal primitives such as +Workflows, or execute arbitrary code. + +Learn more at [temporal.io/nexus](https://temporal.io/nexus). + +The samples in this directory form an introduction to Nexus. + +### Samples + +- [basic](./basic) - Nexus service definition, operation handlers, and calling workflows. +- [without_service_definition](./without_service_definition) - A Nexus service implementation without a service definition \ No newline at end of file diff --git a/hello_nexus/__init__.py b/hello_nexus/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hello_nexus/basic/README.md b/hello_nexus/basic/README.md new file mode 100644 index 00000000..ed6528ab --- /dev/null +++ b/hello_nexus/basic/README.md @@ -0,0 +1,37 @@ +This sample shows how to define a Nexus service, implement the operation handlers, and +call the operations from a workflow. + +### Sample directory structure + +- [service.py](./service.py) - shared Nexus service definition +- [caller](./caller) - a caller workflow that executes Nexus operations, together with a worker and starter code +- [handler](./handler) - Nexus operation handlers, together with a workflow used by one of the Nexus operations, and a worker that polls for both workflow and Nexus tasks. + + +### Instructions + +Start a Temporal server. + +Run the following: + +``` +temporal operator namespace create --namespace my-target-namespace +temporal operator namespace create --namespace my-caller-namespace + +temporal operator nexus endpoint create \ + --name my-nexus-endpoint \ + --target-namespace my-target-namespace \ + --target-task-queue my-target-task-queue \ + --description-file ./hello_nexus/basic/service_description.md +``` + +In one terminal, run the Temporal worker in the handler namespace: +``` +uv run hello_nexus/basic/handler/worker.py +``` + +In another terminal, run the Temporal worker in the caller namespace and start the caller +workflow: +``` +uv run hello_nexus/basic/caller/app.py +``` diff --git a/hello_nexus/basic/__init__.py b/hello_nexus/basic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hello_nexus/basic/caller/__init__.py b/hello_nexus/basic/caller/__init__.py new file mode 100644 index 00000000..cf63d39c --- /dev/null +++ b/hello_nexus/basic/caller/__init__.py @@ -0,0 +1 @@ +from . import app as app diff --git a/hello_nexus/basic/caller/app.py b/hello_nexus/basic/caller/app.py new file mode 100644 index 00000000..3e1f95ad --- /dev/null +++ b/hello_nexus/basic/caller/app.py @@ -0,0 +1,46 @@ +import asyncio +import uuid +from typing import Optional + +from temporalio.client import Client +from temporalio.worker import UnsandboxedWorkflowRunner, Worker + +from hello_nexus.basic.caller.workflows import CallerWorkflow +from hello_nexus.basic.service import MyOutput + +NAMESPACE = "my-caller-namespace" +TASK_QUEUE = "my-caller-task-queue" + + +async def execute_caller_workflow( + client: Optional[Client] = None, +) -> tuple[MyOutput, MyOutput]: + client = client or await Client.connect( + "localhost:7233", + namespace=NAMESPACE, + ) + + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[CallerWorkflow], + # TODO(dan): isinstance(op, nexusrpc.contract.Operation) is failing under the + # sandbox in temporalio/worker/_interceptor.py + workflow_runner=UnsandboxedWorkflowRunner(), + ): + return await client.execute_workflow( + CallerWorkflow.run, + arg="world", + id=str(uuid.uuid4()), + task_queue=TASK_QUEUE, + ) + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + try: + results = loop.run_until_complete(execute_caller_workflow()) + for output in results: + print(output.message) + except KeyboardInterrupt: + loop.run_until_complete(loop.shutdown_asyncgens()) diff --git a/hello_nexus/basic/caller/workflows.py b/hello_nexus/basic/caller/workflows.py new file mode 100644 index 00000000..1108a4f4 --- /dev/null +++ b/hello_nexus/basic/caller/workflows.py @@ -0,0 +1,35 @@ +from temporalio import workflow +from temporalio.workflow import NexusClient + +from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput + +NEXUS_ENDPOINT = "my-nexus-endpoint" + + +# This is a workflow that calls a nexus operation. +@workflow.defn +class CallerWorkflow: + # An __init__ method is always optional on a Workflow class. Here we use it to set the + # NexusClient, but that could alternatively be done in the run method. + def __init__(self): + self.nexus_client = NexusClient( + MyNexusService, + endpoint=NEXUS_ENDPOINT, + ) + + # The Wokflow run method invokes two Nexus operations. + @workflow.run + async def run(self, name: str) -> tuple[MyOutput, MyOutput]: + # Start the Nexus operation and wait for the result in one go, using execute_operation. + wf_result: MyOutput = await self.nexus_client.execute_operation( + MyNexusService.my_workflow_run_operation, + MyInput(name), + ) + # We could use execute_operation for this one also, but here we demonstrate + # obtaining the operation handle and then using it to get the result. + sync_operation_handle = await self.nexus_client.start_operation( + MyNexusService.my_sync_operation, + MyInput(name), + ) + sync_result = await sync_operation_handle + return sync_result, wf_result diff --git a/hello_nexus/basic/handler/__init__.py b/hello_nexus/basic/handler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hello_nexus/basic/handler/db_client.py b/hello_nexus/basic/handler/db_client.py new file mode 100644 index 00000000..fac38a1d --- /dev/null +++ b/hello_nexus/basic/handler/db_client.py @@ -0,0 +1,23 @@ +from __future__ import annotations + + +class MyDBClient: + """ + This class represents a resource that your Nexus operation handlers may need when they + are handling Nexus requests, but which is only available when the Nexus worker is + started. Notice that: + + (a) The user's service handler class __init__ constructor takes a MyDBClient instance + (see hello_nexus.handler.MyNexusService) + + (b) The user is responsible for instantiating the service handler class when they + start the worker (see hello_nexus.handler.worker), so they can pass any + necessary resources (such as this database client) to the service handler. + """ + + @classmethod + def connect(cls) -> MyDBClient: + return cls() + + def execute(self, query: str) -> str: + return "query-result" diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py new file mode 100644 index 00000000..4461d0f8 --- /dev/null +++ b/hello_nexus/basic/handler/service_handler.py @@ -0,0 +1,69 @@ +""" +This file demonstrates how to define operation handlers by using the "shorthand" +decorators sync_operation_handler and workflow_run_operation_handler. In this style you +implement the `start` method only. workflow_run_operation_handler implements `cancel` for +you automatically, but apart from that, the other operation methods (`fetch_info`, +`fetch_result`, and `cancel` for sync_operation_handler) are all automatically created +with "raise NotImplementedError" implementations. + +See hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py for the +alternative "fully manual" style where you implement an OperationHandler class directly. +""" + +from __future__ import annotations + +import uuid + +import temporalio.common +import temporalio.nexus.handler +from nexusrpc.handler import ( + StartOperationContext, + service_handler, + sync_operation_handler, +) +from temporalio.client import WorkflowHandle + +from hello_nexus.basic.handler.db_client import MyDBClient +from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation +from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput + + +@service_handler(service=MyNexusService) +class MyNexusServiceHandler: + # You can create an __init__ method accepting what is needed by your operation + # handlers to handle requests. You typically instantiate your service handler class + # when starting your worker. See hello_nexus/basic/handler/worker.py. + def __init__(self, connected_db_client: MyDBClient): + # `connected_db_client` is intended as an example of something that might be + # required by your operation handlers when handling requests, but is only + # available at worker-start time. + self.connected_db_client = connected_db_client + + # This is a nexus operation that is backed by a Temporal workflow. The start method + # starts a workflow, and returns a nexus operation token that the handler can use to + # obtain a workflow handle (for example if a cancel request is subsequently sent by + # the caller). The Temporal server takes care of delivering the workflow result to the + # calling workflow. The task queue defaults to the task queue being used by the Nexus + # worker. + @temporalio.nexus.handler.workflow_run_operation_handler + async def my_workflow_run_operation( + self, ctx: StartOperationContext, input: MyInput + ) -> WorkflowHandle[WorkflowStartedByNexusOperation, MyOutput]: + # You could use self.connected_db_client here. + return await temporalio.nexus.handler.start_workflow( + ctx, + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + ) + + # This is a sync operation. That means that unlike the workflow run operation above, + # in this case the `start` method returns the final operation result. Sync operations + # are free to make arbitrary network calls, or perform CPU-bound computations. Total + # execution duration must not exceed 10s. + @sync_operation_handler + async def my_sync_operation( + self, ctx: StartOperationContext, input: MyInput + ) -> MyOutput: + # You could use self.connected_db_client here. + return MyOutput(message=f"Hello {input.name} from sync operation!") diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py new file mode 100644 index 00000000..d81632f8 --- /dev/null +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -0,0 +1,155 @@ +""" +This file demonstrates how to define operation handlers by implementing an OperationHandler +class directly. + +See hello_nexus/basic/handler/service_handler.py for the alternative "shorthand" style +where you implement the `start` method only. + +Sync operations: +--------------- +Implementations are free to make arbitrary network calls, or perform CPU-bound +computations such as this one. Total execution duration must not exceed 10s. + + +Workflow operations: +--------------------- +The task queue defaults to the task queue being used by the Nexus worker. +""" + +from __future__ import annotations + +import uuid + +import temporalio.common +import temporalio.nexus.handler +from nexusrpc.handler import ( + CancelOperationContext, + FetchOperationInfoContext, + FetchOperationResultContext, + OperationHandler, + OperationInfo, + StartOperationContext, + StartOperationResultSync, + operation_handler, + service_handler, +) +from temporalio.nexus.handler import WorkflowRunOperationResult + +from hello_nexus.basic.handler.db_client import MyDBClient +from hello_nexus.basic.handler.service_handler import MyInput, MyNexusService, MyOutput +from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation + + +@service_handler(service=MyNexusService) +class MyNexusServiceHandlerUsingOperationHandlerClasses: + # You can create an __init__ method accepting what is needed by your operation + # handlers to handle requests. You typically instantiate your service handler class + # when starting your worker. See hello_nexus/basic/handler/worker.py. + def __init__(self, connected_db_client: MyDBClient): + # `connected_db_client` is intended as an example of something that might be + # required by your operation handlers when handling requests, but is only + # available at worker-start time. + self.connected_db_client = connected_db_client + + @operation_handler + def my_sync_operation(self) -> OperationHandler[MyInput, MyOutput]: + # Pass any required arguments to the OperationHandler __init__ method here. + return MySyncOperation() + + @operation_handler + def my_workflow_run_operation( + self, + ) -> OperationHandler[MyInput, MyOutput]: + # Pass any required arguments to the OperationHandler __init__ method here. + return MyWorkflowRunOperation() + + +# This is a Nexus operation that responds synchronously to all requests. +class MySyncOperation(OperationHandler[MyInput, MyOutput]): + # You can add an __init__ method taking any required arguments, since you are in + # control of instantiating the OperationHandler inside the operation handler method + # above decorated with @operation_handler. + + # Unlike the workflow run operation below, the `start` method for a sync operation + # returns the final operation result. Sync operations are free to make arbitrary + # network calls, or perform CPU-bound computations. Total execution duration must not + # exceed 10s. async def start( + async def start( + self, ctx: StartOperationContext, input: MyInput + ) -> StartOperationResultSync[MyOutput]: + output = MyOutput(message=f"Hello {input.name} from sync operation!") + return StartOperationResultSync(output) + + async def fetch_info( + self, + ctx: FetchOperationInfoContext, + token: str, + ) -> OperationInfo: + raise NotImplementedError( + "fetch_info is not supported when a Nexus operation is called by a Temporal workflow" + ) + + async def fetch_result( + self, + ctx: FetchOperationResultContext, + token: str, + ) -> MyOutput: + raise NotImplementedError( + "fetch_result is not supported when a Nexus operation is called by a Temporal workflow, " + "but this sample does not demonstrate result fetching" + ) + + async def cancel( + self, + ctx: CancelOperationContext, + token: str, + ) -> None: + raise NotImplementedError( + "cancel is supported when a Nexus operation is called by a Temporal workflow, " + "but this sample does not demonstrate cancellation" + ) + + +# This is a Nexus operation that is backed by a Temporal workflow. That means that it +# responds asynchronously to all requests: it starts a workflow and responds with a token +# that the handler can associate with the worklow is started. +class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): + # You can add an __init__ method taking any required arguments, since you are in + # control of instantiating the OperationHandler inside the operation handler method + # above decorated with @operation_handler. + + # The start method starts a workflow, and returns a WorkflowRunOperationResult that it + # creates from the workflow handle. This return value contains the Nexus operation + # token that the handler can use to obtain a handle and interact with the workflow on + # future requests (for example if a cancel request is subsequently sent by the + # caller). The Temporal server takes care of delivering the workflow result to the + # calling workflow. The task queue defaults to the task queue being used by the Nexus + # worker. + async def start( + self, ctx: StartOperationContext, input: MyInput + ) -> WorkflowRunOperationResult: + wf_handle = await temporalio.nexus.handler.start_workflow( + ctx, + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + ) + return WorkflowRunOperationResult.from_workflow_handle(wf_handle) + + async def cancel(self, ctx: CancelOperationContext, token: str) -> None: + return await temporalio.nexus.handler.cancel_workflow(ctx, token) + + async def fetch_info( + self, ctx: FetchOperationInfoContext, token: str + ) -> OperationInfo: + raise NotImplementedError( + "fetch_info is not supported when a Nexus operation is called by a Temporal workflow" + ) + + async def fetch_result( + self, ctx: FetchOperationResultContext, token: str + ) -> MyOutput: + raise NotImplementedError( + "fetch_result is not supported when a Nexus operation is called by a Temporal workflow, " + "but this sample does not demonstrate result fetching" + ) diff --git a/hello_nexus/basic/handler/worker.py b/hello_nexus/basic/handler/worker.py new file mode 100644 index 00000000..8e3f122c --- /dev/null +++ b/hello_nexus/basic/handler/worker.py @@ -0,0 +1,68 @@ +import asyncio +import logging +from typing import Optional + +from temporalio.client import Client +from temporalio.worker import Worker + +from hello_nexus.basic.handler.db_client import MyDBClient +from hello_nexus.basic.handler.service_handler import MyNexusServiceHandler +from hello_nexus.basic.handler.service_handler_with_operation_handler_classes import ( + MyNexusServiceHandlerUsingOperationHandlerClasses, +) +from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation + +interrupt_event = asyncio.Event() + +NAMESPACE = "my-target-namespace" +TASK_QUEUE = "my-target-task-queue" + + +async def main( + client: Optional[Client] = None, + # Change this to use the service handler defined in + # hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py + use_operation_handler_classes: bool = False, +): + logging.basicConfig(level=logging.INFO) + + client = client or await Client.connect( + "localhost:7233", + namespace=NAMESPACE, + ) + + # Create an instance of the service handler. Your service handler class __init__ can + # be written to accept any arguments that your operation handlers need when handling + # requests. In this example we provide a database client object to the service hander. + connected_db_client = MyDBClient.connect() + + my_nexus_service_handler = ( + MyNexusServiceHandlerUsingOperationHandlerClasses( + connected_db_client=connected_db_client + ) + if use_operation_handler_classes + else MyNexusServiceHandler(connected_db_client=connected_db_client) + ) + + # Start the worker, passing the Nexus service handler instance, in addition to the + # workflow classes that are started by your nexus operations, and any activities + # needed. This Worker will poll for both workflow tasks and Nexus tasks (this example + # doesn't use any activities). + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[WorkflowStartedByNexusOperation], + nexus_services=[my_nexus_service_handler], + ): + logging.info("Worker started, ctrl+c to exit") + await interrupt_event.wait() + logging.info("Shutting down") + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(main()) + except KeyboardInterrupt: + interrupt_event.set() + loop.run_until_complete(loop.shutdown_asyncgens()) diff --git a/hello_nexus/basic/handler/workflows.py b/hello_nexus/basic/handler/workflows.py new file mode 100644 index 00000000..e21e6aaa --- /dev/null +++ b/hello_nexus/basic/handler/workflows.py @@ -0,0 +1,14 @@ +from temporalio import workflow + +from hello_nexus.basic.service import MyInput, MyOutput + + +@workflow.defn +class WorkflowStartedByNexusOperation: + """ + This is the workflow that is started by the `my_workflow_run_operation` nexus operation. + """ + + @workflow.run + async def run(self, input: MyInput) -> MyOutput: + return MyOutput(message=f"Hello {input.name} from workflow run operation!") diff --git a/hello_nexus/basic/service.py b/hello_nexus/basic/service.py new file mode 100644 index 00000000..6528775d --- /dev/null +++ b/hello_nexus/basic/service.py @@ -0,0 +1,33 @@ +""" +This is a Nexus service definition. + +A service definition defines a Nexus service as a named collection of operations, each +with input and output types. It does not implement operation handling: see the service +handler and operation handlers in hello_nexus.handler.nexus_service for that. + +A Nexus service definition is used by Nexus callers (e.g. a Temporal workflow) to create +type-safe clients, and it is used by Nexus handlers to validate that they implement +correctly-named operation handlers with the correct input and output types. + +The service defined in this file features two operations: echo and hello. +""" + +from dataclasses import dataclass + +import nexusrpc + + +@dataclass +class MyInput: + name: str + + +@dataclass +class MyOutput: + message: str + + +@nexusrpc.service +class MyNexusService: + my_sync_operation: nexusrpc.Operation[MyInput, MyOutput] + my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput] diff --git a/hello_nexus/basic/service_description.md b/hello_nexus/basic/service_description.md new file mode 100644 index 00000000..9a381cd0 --- /dev/null +++ b/hello_nexus/basic/service_description.md @@ -0,0 +1,3 @@ +## Service: [MyNexusService](https://github.com/temporalio/samples-python/blob/main/hello_nexus/basic/service.py) + - operation: `my_sync_operation` + - operation: `my_workflow_run_operation` diff --git a/hello_nexus/without_service_definition/README.md b/hello_nexus/without_service_definition/README.md new file mode 100644 index 00000000..1f764ab3 --- /dev/null +++ b/hello_nexus/without_service_definition/README.md @@ -0,0 +1,25 @@ +Usually you will want to create a service definition to formalize the service contract. +However it is possible to define a Nexus service and operation handlers without creating a +service definition. This sample demonstrates how to do that. This may be appropriate if +you want to call a Nexus operation that is being executed by a Worker in the same +namespace as the caller: in other words, if the Nexus operation is playing a role similar +to an Activity. + +### Instructions + +Start a Temporal server. + +Run the following: + +``` +temporal operator namespace create --namespace my-namespace +temporal operator nexus endpoint create \ + --name my-nexus-endpoint \ + --target-namespace my-namespace \ + --target-task-queue my-task-queue +``` + +From the root of the repo, run +``` +uv run hello_nexus/without_service_definition/app.py +``` diff --git a/hello_nexus/without_service_definition/__init__.py b/hello_nexus/without_service_definition/__init__.py new file mode 100644 index 00000000..cf63d39c --- /dev/null +++ b/hello_nexus/without_service_definition/__init__.py @@ -0,0 +1 @@ +from . import app as app diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py new file mode 100644 index 00000000..5ff4a898 --- /dev/null +++ b/hello_nexus/without_service_definition/app.py @@ -0,0 +1,104 @@ +""" +This file demonstrates running a nexus service in the same namespace as the workflow that +is calling the nexus service, without specifying a separate service definition. +""" + +from __future__ import annotations + +import asyncio +import uuid +from typing import Optional + +import temporalio.nexus.handler +from nexusrpc.handler import ( + StartOperationContext, + service_handler, +) +from temporalio import workflow +from temporalio.client import Client, WorkflowHandle +from temporalio.nexus.handler import workflow_run_operation_handler +from temporalio.worker import UnsandboxedWorkflowRunner, Worker +from temporalio.workflow import NexusClient + +NAMESPACE = "my-namespace" +TASK_QUEUE = "my-task-queue" +NEXUS_ENDPOINT = "my-nexus-endpoint" + +# +# Handler +# + + +@workflow.defn +class HandlerWorkflow: + @workflow.run + async def run(self, message: str) -> str: + return f"Hello {message} from workflow run operation!" + + +# Here we define a nexus service by providing a service handler implementation without a +# service contract. +@service_handler +class MyNexusServiceHandler: + # The nexus service has one operation. When using the workflow_run_operation_handler + # decorator, your start method must return a WorkflowHandle directly, using the + # temporalio.nexus.handler.start_workflow helper. (Temporal server takes care of + # delivering the workflow result to the caller, using the Nexus RPC callback mechanism). + @workflow_run_operation_handler + async def my_workflow_run_operation( + self, ctx: StartOperationContext, name: str + ) -> WorkflowHandle[HandlerWorkflow, str]: + return await temporalio.nexus.handler.start_workflow( + ctx, HandlerWorkflow.run, name, id=str(uuid.uuid4()) + ) + + +# +# Caller +# + + +@workflow.defn +class CallerWorkflow: + @workflow.run + async def run(self, message: str) -> str: + # Create the type-safe workflow nexus service client, and invoke the nexus + # operation. + # + # Normally, the first argument to both these calls would reference a service + # contract class, but they can also reference your service handler class, as here. + + nexus_client = NexusClient(MyNexusServiceHandler, endpoint=NEXUS_ENDPOINT) + return await nexus_client.execute_operation( + MyNexusServiceHandler.my_workflow_run_operation, message + ) + + +async def execute_caller_workflow(client: Optional[Client] = None) -> str: + client = client or await Client.connect("localhost:7233", namespace=NAMESPACE) + # Start a worker that polls for tasks for both the caller workflow and the nexus + # service. + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[CallerWorkflow, HandlerWorkflow], + nexus_services=[MyNexusServiceHandler()], + # TODO(dan): isinstance(op, nexusrpc.contract.Operation) is failing under the + # sandbox in temporalio/worker/_interceptor.py + workflow_runner=UnsandboxedWorkflowRunner(), + ): + return await client.execute_workflow( + CallerWorkflow.run, + "world", + id=str(uuid.uuid4()), + task_queue=TASK_QUEUE, + ) + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + try: + result = loop.run_until_complete(execute_caller_workflow()) + print(result) + except KeyboardInterrupt: + loop.run_until_complete(loop.shutdown_asyncgens()) diff --git a/open_telemetry/worker.py b/open_telemetry/worker.py index 4b344123..04095ca7 100644 --- a/open_telemetry/worker.py +++ b/open_telemetry/worker.py @@ -3,7 +3,7 @@ from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.resources import SERVICE_NAME, Resource # type: ignore from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from temporalio import activity, workflow diff --git a/pyproject.toml b/pyproject.toml index b26457f9..6c0730b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,9 @@ dev = [ "pytest>=7.1.2,<8", "pytest-asyncio>=0.18.3,<0.19", "frozenlist>=1.4.0,<2", + "pyright>=1.1.394", "types-pyyaml>=6.0.12.20241230,<7", + "pytest-pretty>=1.3.0", ] bedrock = ["boto3>=1.34.92,<2"] dsl = [ @@ -44,9 +46,12 @@ langchain = [ "tqdm>=4.62.0,<5", "uvicorn[standard]>=0.24.0.post1,<0.25", ] +nexus = [ + "nexus-rpc", +] open-telemetry = [ "temporalio[opentelemetry]", - "opentelemetry-exporter-otlp-proto-grpc==1.18.0", + "opentelemetry-exporter-otlp-proto-grpc", ] openai-agents = [ "openai-agents >= 0.0.19", @@ -73,12 +78,17 @@ default-groups = [ "encryption", "gevent", "langchain", + "nexus", "open-telemetry", "pydantic-converter", "sentry", "trio-async", ] +[tool.uv.sources] +nexus-rpc = { path = "../nexus-sdk-python", editable = true } +temporalio = { path = "../sdk-python", editable = true } + [tool.hatch.build.targets.sdist] include = ["./**/*.py"] @@ -96,6 +106,7 @@ packages = [ "encryption", "gevent_async", "hello", + "hello_nexus", "langchain", "message_passing", "open_telemetry", diff --git a/tests/hello_nexus/basic_test.py b/tests/hello_nexus/basic_test.py new file mode 100644 index 00000000..e66e0b7f --- /dev/null +++ b/tests/hello_nexus/basic_test.py @@ -0,0 +1,45 @@ +import asyncio + +from temporalio.client import Client + +import hello_nexus.basic.caller.app +import hello_nexus.basic.caller.workflows +import hello_nexus.basic.handler.worker +import hello_nexus.without_service_definition.app +from tests.hello_nexus.helpers import create_nexus_endpoint, delete_nexus_endpoint + + +async def test_nexus_service_basic(client: Client): + create_response = await create_nexus_endpoint( + name=hello_nexus.basic.caller.workflows.NEXUS_ENDPOINT, + task_queue=hello_nexus.basic.handler.worker.TASK_QUEUE, + client=client, + ) + try: + for use_operation_handler_classes in [True, False]: + handler_worker_task = asyncio.create_task( + hello_nexus.basic.handler.worker.main( + client, + use_operation_handler_classes=use_operation_handler_classes, + ) + ) + await asyncio.sleep(1) + results = await hello_nexus.basic.caller.app.execute_caller_workflow( + client, + ) + hello_nexus.basic.handler.worker.interrupt_event.set() + await handler_worker_task + hello_nexus.basic.handler.worker.interrupt_event.clear() + print("\n\n") + print([r.message for r in results]) + print("\n\n") + assert [r.message for r in results] == [ + "Hello world from sync operation!", + "Hello world from workflow run operation!", + ] + finally: + await delete_nexus_endpoint( + id=create_response.endpoint.id, + version=create_response.endpoint.version, + client=client, + ) diff --git a/tests/hello_nexus/helpers.py b/tests/hello_nexus/helpers.py new file mode 100644 index 00000000..99be41ef --- /dev/null +++ b/tests/hello_nexus/helpers.py @@ -0,0 +1,41 @@ +import temporalio.api +import temporalio.api.common +import temporalio.api.common.v1 +import temporalio.api.enums.v1 +import temporalio.api.nexus +import temporalio.api.nexus.v1 +import temporalio.api.operatorservice +import temporalio.api.operatorservice.v1 +import temporalio.nexus +import temporalio.nexus.handler +from temporalio.client import Client + + +# TODO: copied from sdk-python tests/helpers/nexus +async def create_nexus_endpoint( + name: str, task_queue: str, client: Client +) -> temporalio.api.operatorservice.v1.CreateNexusEndpointResponse: + return await client.operator_service.create_nexus_endpoint( + temporalio.api.operatorservice.v1.CreateNexusEndpointRequest( + spec=temporalio.api.nexus.v1.EndpointSpec( + name=name, + target=temporalio.api.nexus.v1.EndpointTarget( + worker=temporalio.api.nexus.v1.EndpointTarget.Worker( + namespace=client.namespace, + task_queue=task_queue, + ) + ), + ) + ) + ) + + +async def delete_nexus_endpoint( + id: str, version: int, client: Client +) -> temporalio.api.operatorservice.v1.DeleteNexusEndpointResponse: + return await client.operator_service.delete_nexus_endpoint( + temporalio.api.operatorservice.v1.DeleteNexusEndpointRequest( + id=id, + version=version, + ) + ) diff --git a/tests/hello_nexus/without_workflow_service_test.py b/tests/hello_nexus/without_workflow_service_test.py new file mode 100644 index 00000000..1c4127ee --- /dev/null +++ b/tests/hello_nexus/without_workflow_service_test.py @@ -0,0 +1,28 @@ +from temporalio.client import Client + +from hello_nexus.without_service_definition.app import ( + NEXUS_ENDPOINT, + TASK_QUEUE, + execute_caller_workflow, +) +from tests.hello_nexus.helpers import create_nexus_endpoint, delete_nexus_endpoint + + +# TODO(dan): This test is very slow (~10s) compared to tests/hello_nexus/basic_test.py. +# One difference is that in this test there is only one worker, polling for both workflow +# and nexus tasks. +async def test_nexus_service_without_service_definition(client: Client): + create_response = await create_nexus_endpoint( + name=NEXUS_ENDPOINT, + task_queue=TASK_QUEUE, + client=client, + ) + try: + result = await execute_caller_workflow(client) + assert result == "Hello world from sync operation!" + finally: + await delete_nexus_endpoint( + id=create_response.endpoint.id, + version=create_response.endpoint.version, + client=client, + ) From dac59502c15b08a8b23d8495caaffe632adbcef3 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 8 Jun 2025 23:02:18 -0400 Subject: [PATCH 02/46] Install SDKs from github --- .github/workflows/ci.yml | 4 ++++ pyproject.toml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfb1353e..c298ad77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,10 @@ jobs: runsOn: macos-14 runs-on: ${{ matrix.runsOn || matrix.os }} steps: + - uses: arduino/setup-protoc@v3 + with: + version: "23.x" + repo-token: ${{ secrets.GITHUB_TOKEN }} - uses: astral-sh/setup-uv@v5 - name: Print build information run: "echo head_ref: ${{ github.head_ref }}, ref: ${{ github.ref }}, os: ${{ matrix.os }}, python: ${{ matrix.python }}" diff --git a/pyproject.toml b/pyproject.toml index 6c0730b8..8491f0dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,8 +86,8 @@ default-groups = [ ] [tool.uv.sources] -nexus-rpc = { path = "../nexus-sdk-python", editable = true } -temporalio = { path = "../sdk-python", editable = true } +nexus-rpc = { git = "https://github.com/nexus-rpc/sdk-python", branch = "v0" } +temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "nexus" } [tool.hatch.build.targets.sdist] include = ["./**/*.py"] From 306bb34381191320b124f21bee6afde27077c1ff Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 9 Jun 2025 11:42:57 -0400 Subject: [PATCH 03/46] Change to endpoint_description.md --- hello_nexus/basic/README.md | 2 +- .../basic/{service_description.md => endpoint_description.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename hello_nexus/basic/{service_description.md => endpoint_description.md} (100%) diff --git a/hello_nexus/basic/README.md b/hello_nexus/basic/README.md index ed6528ab..5a56f75f 100644 --- a/hello_nexus/basic/README.md +++ b/hello_nexus/basic/README.md @@ -22,7 +22,7 @@ temporal operator nexus endpoint create \ --name my-nexus-endpoint \ --target-namespace my-target-namespace \ --target-task-queue my-target-task-queue \ - --description-file ./hello_nexus/basic/service_description.md + --description-file ./hello_nexus/basic/endpoint_description.md ``` In one terminal, run the Temporal worker in the handler namespace: diff --git a/hello_nexus/basic/service_description.md b/hello_nexus/basic/endpoint_description.md similarity index 100% rename from hello_nexus/basic/service_description.md rename to hello_nexus/basic/endpoint_description.md From 8c6f43b53cfa0478a77b8c4a5aa6da45391c3109 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 9 Jun 2025 11:44:15 -0400 Subject: [PATCH 04/46] Revert "Install SDKs from github" This reverts commit 6789490fccbedc93dd9045cdcb5608eab7c2828d. --- .github/workflows/ci.yml | 4 ---- pyproject.toml | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c298ad77..bfb1353e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,6 @@ jobs: runsOn: macos-14 runs-on: ${{ matrix.runsOn || matrix.os }} steps: - - uses: arduino/setup-protoc@v3 - with: - version: "23.x" - repo-token: ${{ secrets.GITHUB_TOKEN }} - uses: astral-sh/setup-uv@v5 - name: Print build information run: "echo head_ref: ${{ github.head_ref }}, ref: ${{ github.ref }}, os: ${{ matrix.os }}, python: ${{ matrix.python }}" diff --git a/pyproject.toml b/pyproject.toml index 8491f0dc..6c0730b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,8 +86,8 @@ default-groups = [ ] [tool.uv.sources] -nexus-rpc = { git = "https://github.com/nexus-rpc/sdk-python", branch = "v0" } -temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "nexus" } +nexus-rpc = { path = "../nexus-sdk-python", editable = true } +temporalio = { path = "../sdk-python", editable = true } [tool.hatch.build.targets.sdist] include = ["./**/*.py"] From ba2433207fceec974ca2a2bf33194636845207a3 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 9 Jun 2025 11:45:20 -0400 Subject: [PATCH 05/46] Get rid of type hint --- hello_nexus/basic/caller/workflows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hello_nexus/basic/caller/workflows.py b/hello_nexus/basic/caller/workflows.py index 1108a4f4..9c5f8fc9 100644 --- a/hello_nexus/basic/caller/workflows.py +++ b/hello_nexus/basic/caller/workflows.py @@ -21,7 +21,7 @@ def __init__(self): @workflow.run async def run(self, name: str) -> tuple[MyOutput, MyOutput]: # Start the Nexus operation and wait for the result in one go, using execute_operation. - wf_result: MyOutput = await self.nexus_client.execute_operation( + wf_result = await self.nexus_client.execute_operation( MyNexusService.my_workflow_run_operation, MyInput(name), ) From 4b06bb8f96ee8de602b9cb4ab8c0d6fa6e68c1c8 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 9 Jun 2025 11:48:19 -0400 Subject: [PATCH 06/46] Remove unnecessary re-imports --- hello_nexus/basic/caller/__init__.py | 1 - hello_nexus/without_service_definition/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/hello_nexus/basic/caller/__init__.py b/hello_nexus/basic/caller/__init__.py index cf63d39c..e69de29b 100644 --- a/hello_nexus/basic/caller/__init__.py +++ b/hello_nexus/basic/caller/__init__.py @@ -1 +0,0 @@ -from . import app as app diff --git a/hello_nexus/without_service_definition/__init__.py b/hello_nexus/without_service_definition/__init__.py index cf63d39c..e69de29b 100644 --- a/hello_nexus/without_service_definition/__init__.py +++ b/hello_nexus/without_service_definition/__init__.py @@ -1 +0,0 @@ -from . import app as app From d71c0e4c1f8e9bd07530814cf1ddc1434393ad5a Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 9 Jun 2025 11:49:30 -0400 Subject: [PATCH 07/46] Fix test --- tests/hello_nexus/without_workflow_service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hello_nexus/without_workflow_service_test.py b/tests/hello_nexus/without_workflow_service_test.py index 1c4127ee..4739dd8a 100644 --- a/tests/hello_nexus/without_workflow_service_test.py +++ b/tests/hello_nexus/without_workflow_service_test.py @@ -19,7 +19,7 @@ async def test_nexus_service_without_service_definition(client: Client): ) try: result = await execute_caller_workflow(client) - assert result == "Hello world from sync operation!" + assert result == "Hello world from workflow run operation!" finally: await delete_nexus_endpoint( id=create_response.endpoint.id, From fd3dc44a00910067f1717fe4658c9519e737c5bf Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 12 Jun 2025 12:27:46 -0400 Subject: [PATCH 08/46] Update to use Temporal operation contexts --- hello_nexus/basic/handler/service_handler.py | 9 +++++---- ...rvice_handler_with_operation_handler_classes.py | 7 +++---- hello_nexus/without_service_definition/app.py | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 4461d0f8..6ada11bb 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -14,14 +14,15 @@ import uuid +import nexusrpc.handler import temporalio.common import temporalio.nexus.handler from nexusrpc.handler import ( - StartOperationContext, service_handler, sync_operation_handler, ) from temporalio.client import WorkflowHandle +from temporalio.nexus import StartOperationContext from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation @@ -50,11 +51,11 @@ async def my_workflow_run_operation( self, ctx: StartOperationContext, input: MyInput ) -> WorkflowHandle[WorkflowStartedByNexusOperation, MyOutput]: # You could use self.connected_db_client here. - return await temporalio.nexus.handler.start_workflow( - ctx, + return await ctx.client.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), + task_queue=ctx.task_queue, ) # This is a sync operation. That means that unlike the workflow run operation above, @@ -63,7 +64,7 @@ async def my_workflow_run_operation( # execution duration must not exceed 10s. @sync_operation_handler async def my_sync_operation( - self, ctx: StartOperationContext, input: MyInput + self, ctx: nexusrpc.handler.StartOperationContext, input: MyInput ) -> MyOutput: # You could use self.connected_db_client here. return MyOutput(message=f"Hello {input.name} from sync operation!") diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index d81632f8..5fc24589 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -23,16 +23,15 @@ class directly. import temporalio.common import temporalio.nexus.handler from nexusrpc.handler import ( - CancelOperationContext, FetchOperationInfoContext, FetchOperationResultContext, OperationHandler, OperationInfo, - StartOperationContext, StartOperationResultSync, operation_handler, service_handler, ) +from temporalio.nexus import CancelOperationContext, StartOperationContext from temporalio.nexus.handler import WorkflowRunOperationResult from hello_nexus.basic.handler.db_client import MyDBClient @@ -128,11 +127,11 @@ class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): async def start( self, ctx: StartOperationContext, input: MyInput ) -> WorkflowRunOperationResult: - wf_handle = await temporalio.nexus.handler.start_workflow( - ctx, + wf_handle = await ctx.client.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), + task_queue=ctx.task_queue, ) return WorkflowRunOperationResult.from_workflow_handle(wf_handle) diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 5ff4a898..734a8169 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -9,13 +9,10 @@ import uuid from typing import Optional -import temporalio.nexus.handler -from nexusrpc.handler import ( - StartOperationContext, - service_handler, -) +from nexusrpc.handler import service_handler from temporalio import workflow from temporalio.client import Client, WorkflowHandle +from temporalio.nexus import StartOperationContext from temporalio.nexus.handler import workflow_run_operation_handler from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -48,8 +45,11 @@ class MyNexusServiceHandler: async def my_workflow_run_operation( self, ctx: StartOperationContext, name: str ) -> WorkflowHandle[HandlerWorkflow, str]: - return await temporalio.nexus.handler.start_workflow( - ctx, HandlerWorkflow.run, name, id=str(uuid.uuid4()) + return await ctx.client.start_workflow( + HandlerWorkflow.run, + name, + id=str(uuid.uuid4()), + task_queue=ctx.task_queue, ) From bb5114f7b33a4fdac58085c6d82bdb33edab5bea Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 12 Jun 2025 15:31:06 -0400 Subject: [PATCH 09/46] Emphasize that StartOperationContext is from Temporal --- hello_nexus/basic/handler/service_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 6ada11bb..fe51fbea 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -16,13 +16,13 @@ import nexusrpc.handler import temporalio.common +import temporalio.nexus import temporalio.nexus.handler from nexusrpc.handler import ( service_handler, sync_operation_handler, ) from temporalio.client import WorkflowHandle -from temporalio.nexus import StartOperationContext from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation @@ -48,7 +48,7 @@ def __init__(self, connected_db_client: MyDBClient): # worker. @temporalio.nexus.handler.workflow_run_operation_handler async def my_workflow_run_operation( - self, ctx: StartOperationContext, input: MyInput + self, ctx: temporalio.nexus.StartOperationContext, input: MyInput ) -> WorkflowHandle[WorkflowStartedByNexusOperation, MyOutput]: # You could use self.connected_db_client here. return await ctx.client.start_workflow( From 99e518ee596bb72e3b6b87809f1ab2f876ed6554 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 19 Jun 2025 10:19:42 -0400 Subject: [PATCH 10/46] s/target/handler/ --- hello_nexus/basic/README.md | 6 +++--- hello_nexus/basic/handler/worker.py | 4 ++-- hello_nexus/without_service_definition/README.md | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hello_nexus/basic/README.md b/hello_nexus/basic/README.md index 5a56f75f..32f2de98 100644 --- a/hello_nexus/basic/README.md +++ b/hello_nexus/basic/README.md @@ -15,13 +15,13 @@ Start a Temporal server. Run the following: ``` -temporal operator namespace create --namespace my-target-namespace +temporal operator namespace create --namespace my-handler-namespace temporal operator namespace create --namespace my-caller-namespace temporal operator nexus endpoint create \ --name my-nexus-endpoint \ - --target-namespace my-target-namespace \ - --target-task-queue my-target-task-queue \ + --target-namespace my-handler-namespace \ + --target-task-queue my-handler-task-queue \ --description-file ./hello_nexus/basic/endpoint_description.md ``` diff --git a/hello_nexus/basic/handler/worker.py b/hello_nexus/basic/handler/worker.py index 8e3f122c..5288b249 100644 --- a/hello_nexus/basic/handler/worker.py +++ b/hello_nexus/basic/handler/worker.py @@ -14,8 +14,8 @@ interrupt_event = asyncio.Event() -NAMESPACE = "my-target-namespace" -TASK_QUEUE = "my-target-task-queue" +NAMESPACE = "my-handler-namespace" +TASK_QUEUE = "my-handler-task-queue" async def main( diff --git a/hello_nexus/without_service_definition/README.md b/hello_nexus/without_service_definition/README.md index 1f764ab3..37ccdc97 100644 --- a/hello_nexus/without_service_definition/README.md +++ b/hello_nexus/without_service_definition/README.md @@ -15,8 +15,8 @@ Run the following: temporal operator namespace create --namespace my-namespace temporal operator nexus endpoint create \ --name my-nexus-endpoint \ - --target-namespace my-namespace \ - --target-task-queue my-task-queue + --handler-namespace my-namespace \ + --handler-task-queue my-task-queue ``` From the root of the repo, run From 2d7da1945c270bfc9d7b1b68dc58513d5f1cd50f Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 19 Jun 2025 11:37:40 -0400 Subject: [PATCH 11/46] Respond to upstream rename --- hello_nexus/basic/handler/worker.py | 2 +- hello_nexus/without_service_definition/app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hello_nexus/basic/handler/worker.py b/hello_nexus/basic/handler/worker.py index 5288b249..6fb273f6 100644 --- a/hello_nexus/basic/handler/worker.py +++ b/hello_nexus/basic/handler/worker.py @@ -52,7 +52,7 @@ async def main( client, task_queue=TASK_QUEUE, workflows=[WorkflowStartedByNexusOperation], - nexus_services=[my_nexus_service_handler], + nexus_service_handlers=[my_nexus_service_handler], ): logging.info("Worker started, ctrl+c to exit") await interrupt_event.wait() diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 734a8169..3bc7e9e2 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -82,7 +82,7 @@ async def execute_caller_workflow(client: Optional[Client] = None) -> str: client, task_queue=TASK_QUEUE, workflows=[CallerWorkflow, HandlerWorkflow], - nexus_services=[MyNexusServiceHandler()], + nexus_service_handlers=[MyNexusServiceHandler()], # TODO(dan): isinstance(op, nexusrpc.contract.Operation) is failing under the # sandbox in temporalio/worker/_interceptor.py workflow_runner=UnsandboxedWorkflowRunner(), From df61c94f10120654b4e707ca61be14ca29809029 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sat, 21 Jun 2025 21:18:39 -0400 Subject: [PATCH 12/46] Respond to upstream: NexusStartWorkflowRequest --- hello_nexus/basic/handler/service_handler.py | 23 ++++++++++------ ..._handler_with_operation_handler_classes.py | 23 +++++++++++----- hello_nexus/without_service_definition/app.py | 26 ++++++++++++------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index fe51fbea..560bda7f 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -19,10 +19,14 @@ import temporalio.nexus import temporalio.nexus.handler from nexusrpc.handler import ( + StartOperationContext, service_handler, sync_operation_handler, ) -from temporalio.client import WorkflowHandle +from temporalio.nexus.handler import ( + NexusStartWorkflowRequest, + TemporalNexusOperationContext, +) from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation @@ -48,14 +52,17 @@ def __init__(self, connected_db_client: MyDBClient): # worker. @temporalio.nexus.handler.workflow_run_operation_handler async def my_workflow_run_operation( - self, ctx: temporalio.nexus.StartOperationContext, input: MyInput - ) -> WorkflowHandle[WorkflowStartedByNexusOperation, MyOutput]: + self, ctx: StartOperationContext, input: MyInput + ) -> NexusStartWorkflowRequest[MyOutput]: # You could use self.connected_db_client here. - return await ctx.client.start_workflow( - WorkflowStartedByNexusOperation.run, - input, - id=str(uuid.uuid4()), - task_queue=ctx.task_queue, + tctx = TemporalNexusOperationContext.current() + return NexusStartWorkflowRequest( + tctx.client.start_workflow( + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + task_queue=tctx.task_queue, + ) ) # This is a sync operation. That means that unlike the workflow run operation above, diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index 5fc24589..2144ca34 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -23,16 +23,21 @@ class directly. import temporalio.common import temporalio.nexus.handler from nexusrpc.handler import ( + CancelOperationContext, FetchOperationInfoContext, FetchOperationResultContext, OperationHandler, OperationInfo, + StartOperationContext, + StartOperationResultAsync, StartOperationResultSync, operation_handler, service_handler, ) -from temporalio.nexus import CancelOperationContext, StartOperationContext -from temporalio.nexus.handler import WorkflowRunOperationResult +from temporalio.nexus.handler import ( + TemporalNexusOperationContext, + WorkflowOperationToken, +) from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.service_handler import MyInput, MyNexusService, MyOutput @@ -126,14 +131,20 @@ class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): # worker. async def start( self, ctx: StartOperationContext, input: MyInput - ) -> WorkflowRunOperationResult: - wf_handle = await ctx.client.start_workflow( + ) -> StartOperationResultAsync: + tctx = TemporalNexusOperationContext.current() + wf_handle = await tctx.client.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), - task_queue=ctx.task_queue, + task_queue=tctx.task_queue, ) - return WorkflowRunOperationResult.from_workflow_handle(wf_handle) + # TODO(prerelease) It must be possible to start "normal" workflows in here, and + # then finish up with a "nexusified" workflow. It should not be possible to + # construct a Nexus token for a non-nexusified workflow. + + token = WorkflowOperationToken.from_workflow_handle(wf_handle).encode() + return StartOperationResultAsync(token) async def cancel(self, ctx: CancelOperationContext, token: str) -> None: return await temporalio.nexus.handler.cancel_workflow(ctx, token) diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 3bc7e9e2..d42708ab 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -9,11 +9,14 @@ import uuid from typing import Optional -from nexusrpc.handler import service_handler +from nexusrpc.handler import StartOperationContext, service_handler from temporalio import workflow -from temporalio.client import Client, WorkflowHandle -from temporalio.nexus import StartOperationContext -from temporalio.nexus.handler import workflow_run_operation_handler +from temporalio.client import Client +from temporalio.nexus.handler import ( + NexusStartWorkflowRequest, + TemporalNexusOperationContext, + workflow_run_operation_handler, +) from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -44,12 +47,15 @@ class MyNexusServiceHandler: @workflow_run_operation_handler async def my_workflow_run_operation( self, ctx: StartOperationContext, name: str - ) -> WorkflowHandle[HandlerWorkflow, str]: - return await ctx.client.start_workflow( - HandlerWorkflow.run, - name, - id=str(uuid.uuid4()), - task_queue=ctx.task_queue, + ) -> NexusStartWorkflowRequest[str]: + tctx = TemporalNexusOperationContext.current() + return NexusStartWorkflowRequest( + tctx.client.start_workflow( + HandlerWorkflow.run, + name, + id=str(uuid.uuid4()), + task_queue=tctx.task_queue, + ) ) From 306418d76c4f26c9f786dfd271ef6d455beb50b3 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 22 Jun 2025 09:41:01 -0400 Subject: [PATCH 13/46] Respond to upstream: tctc.start_workflow -> WorkflowOperationToken --- hello_nexus/basic/handler/service_handler.py | 29 ++++++++++--------- ..._handler_with_operation_handler_classes.py | 11 ++----- hello_nexus/without_service_definition/app.py | 17 +++++------ 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 560bda7f..2248fec4 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -24,8 +24,8 @@ sync_operation_handler, ) from temporalio.nexus.handler import ( - NexusStartWorkflowRequest, TemporalNexusOperationContext, + WorkflowOperationToken, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -45,24 +45,25 @@ def __init__(self, connected_db_client: MyDBClient): self.connected_db_client = connected_db_client # This is a nexus operation that is backed by a Temporal workflow. The start method - # starts a workflow, and returns a nexus operation token that the handler can use to - # obtain a workflow handle (for example if a cancel request is subsequently sent by - # the caller). The Temporal server takes care of delivering the workflow result to the - # calling workflow. The task queue defaults to the task queue being used by the Nexus - # worker. + # starts a workflow, and returns a nexus operation token synchronously. Meanwhile, + # the workflow executes in the background, and the Temporal server takes care of + # delivering the eventual workflow result (success or failure) to the calling + # workflow. + # + # The token will be used by the caller if it subsequently wants to cancel the Nexus + # operation. @temporalio.nexus.handler.workflow_run_operation_handler async def my_workflow_run_operation( self, ctx: StartOperationContext, input: MyInput - ) -> NexusStartWorkflowRequest[MyOutput]: + ) -> WorkflowOperationToken[MyOutput]: # You could use self.connected_db_client here. tctx = TemporalNexusOperationContext.current() - return NexusStartWorkflowRequest( - tctx.client.start_workflow( - WorkflowStartedByNexusOperation.run, - input, - id=str(uuid.uuid4()), - task_queue=tctx.task_queue, - ) + return await tctx.start_workflow( + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + client=tctx.client, + task_queue=tctx.task_queue, ) # This is a sync operation. That means that unlike the workflow run operation above, diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index 2144ca34..cacac915 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -36,7 +36,6 @@ class directly. ) from temporalio.nexus.handler import ( TemporalNexusOperationContext, - WorkflowOperationToken, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -133,18 +132,14 @@ async def start( self, ctx: StartOperationContext, input: MyInput ) -> StartOperationResultAsync: tctx = TemporalNexusOperationContext.current() - wf_handle = await tctx.client.start_workflow( + token = await tctx.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), + client=tctx.client, task_queue=tctx.task_queue, ) - # TODO(prerelease) It must be possible to start "normal" workflows in here, and - # then finish up with a "nexusified" workflow. It should not be possible to - # construct a Nexus token for a non-nexusified workflow. - - token = WorkflowOperationToken.from_workflow_handle(wf_handle).encode() - return StartOperationResultAsync(token) + return StartOperationResultAsync(token.encode()) async def cancel(self, ctx: CancelOperationContext, token: str) -> None: return await temporalio.nexus.handler.cancel_workflow(ctx, token) diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index d42708ab..093376ab 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -13,8 +13,8 @@ from temporalio import workflow from temporalio.client import Client from temporalio.nexus.handler import ( - NexusStartWorkflowRequest, TemporalNexusOperationContext, + WorkflowOperationToken, workflow_run_operation_handler, ) from temporalio.worker import UnsandboxedWorkflowRunner, Worker @@ -47,15 +47,14 @@ class MyNexusServiceHandler: @workflow_run_operation_handler async def my_workflow_run_operation( self, ctx: StartOperationContext, name: str - ) -> NexusStartWorkflowRequest[str]: + ) -> WorkflowOperationToken[str]: tctx = TemporalNexusOperationContext.current() - return NexusStartWorkflowRequest( - tctx.client.start_workflow( - HandlerWorkflow.run, - name, - id=str(uuid.uuid4()), - task_queue=tctx.task_queue, - ) + return await tctx.start_workflow( + HandlerWorkflow.run, + name, + id=str(uuid.uuid4()), + client=tctx.client, + task_queue=tctx.task_queue, ) From ad54378b224963a213036fb8a761ba19fd5b050b Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 23 Jun 2025 12:32:52 -0400 Subject: [PATCH 14/46] Respond to upstream: use factories instead of decorators --- hello_nexus/basic/handler/service_handler.py | 66 ++++++++++--------- ..._handler_with_operation_handler_classes.py | 11 ++-- hello_nexus/without_service_definition/app.py | 49 ++++++++------ 3 files changed, 68 insertions(+), 58 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 2248fec4..e2013119 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -1,10 +1,9 @@ """ -This file demonstrates how to define operation handlers by using the "shorthand" -decorators sync_operation_handler and workflow_run_operation_handler. In this style you -implement the `start` method only. workflow_run_operation_handler implements `cancel` for -you automatically, but apart from that, the other operation methods (`fetch_info`, -`fetch_result`, and `cancel` for sync_operation_handler) are all automatically created -with "raise NotImplementedError" implementations. +This file demonstrates how to define operation handlers by using a "shorthand" style in +which you implement the `start` method only. WorkflowRunOperationHandler implements +`cancel` for you automatically, but apart from that, the other operation methods +(`fetch_info`, `fetch_result`, and `cancel` for SyncOperationHandler) are all +automatically created with "raise NotImplementedError" implementations. See hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py for the alternative "fully manual" style where you implement an OperationHandler class directly. @@ -14,18 +13,17 @@ import uuid -import nexusrpc.handler -import temporalio.common -import temporalio.nexus -import temporalio.nexus.handler from nexusrpc.handler import ( + OperationHandler, StartOperationContext, + SyncOperationHandler, + operation_handler, service_handler, - sync_operation_handler, ) from temporalio.nexus.handler import ( - TemporalNexusOperationContext, + TemporalOperationContext, WorkflowOperationToken, + WorkflowRunOperationHandler, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -52,27 +50,33 @@ def __init__(self, connected_db_client: MyDBClient): # # The token will be used by the caller if it subsequently wants to cancel the Nexus # operation. - @temporalio.nexus.handler.workflow_run_operation_handler - async def my_workflow_run_operation( - self, ctx: StartOperationContext, input: MyInput - ) -> WorkflowOperationToken[MyOutput]: - # You could use self.connected_db_client here. - tctx = TemporalNexusOperationContext.current() - return await tctx.start_workflow( - WorkflowStartedByNexusOperation.run, - input, - id=str(uuid.uuid4()), - client=tctx.client, - task_queue=tctx.task_queue, - ) + @operation_handler + def my_workflow_run_operation( + self, + ) -> OperationHandler[MyInput, MyOutput]: + async def start( + ctx: StartOperationContext, input: MyInput + ) -> WorkflowOperationToken[MyOutput]: + # You could use self.connected_db_client here. + tctx = TemporalOperationContext.current() + return await tctx.start_workflow( + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + ) + + return WorkflowRunOperationHandler(start) # This is a sync operation. That means that unlike the workflow run operation above, # in this case the `start` method returns the final operation result. Sync operations # are free to make arbitrary network calls, or perform CPU-bound computations. Total # execution duration must not exceed 10s. - @sync_operation_handler - async def my_sync_operation( - self, ctx: nexusrpc.handler.StartOperationContext, input: MyInput - ) -> MyOutput: - # You could use self.connected_db_client here. - return MyOutput(message=f"Hello {input.name} from sync operation!") + @operation_handler + def my_sync_operation( + self, + ) -> OperationHandler[MyInput, MyOutput]: + async def start(ctx: StartOperationContext, input: MyInput) -> MyOutput: + # You could use self.connected_db_client here. + return MyOutput(message=f"Hello {input.name} from sync operation!") + + return SyncOperationHandler(start) diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index cacac915..4c3e872d 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -20,8 +20,6 @@ class directly. import uuid -import temporalio.common -import temporalio.nexus.handler from nexusrpc.handler import ( CancelOperationContext, FetchOperationInfoContext, @@ -35,7 +33,8 @@ class directly. service_handler, ) from temporalio.nexus.handler import ( - TemporalNexusOperationContext, + TemporalOperationContext, + cancel_operation, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -131,18 +130,16 @@ class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): async def start( self, ctx: StartOperationContext, input: MyInput ) -> StartOperationResultAsync: - tctx = TemporalNexusOperationContext.current() + tctx = TemporalOperationContext.current() token = await tctx.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), - client=tctx.client, - task_queue=tctx.task_queue, ) return StartOperationResultAsync(token.encode()) async def cancel(self, ctx: CancelOperationContext, token: str) -> None: - return await temporalio.nexus.handler.cancel_workflow(ctx, token) + return await cancel_operation(token) async def fetch_info( self, ctx: FetchOperationInfoContext, token: str diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 093376ab..bf357f7d 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -9,13 +9,18 @@ import uuid from typing import Optional -from nexusrpc.handler import StartOperationContext, service_handler +from nexusrpc.handler import ( + OperationHandler, + StartOperationContext, + operation_handler, + service_handler, +) from temporalio import workflow from temporalio.client import Client from temporalio.nexus.handler import ( - TemporalNexusOperationContext, + TemporalOperationContext, WorkflowOperationToken, - workflow_run_operation_handler, + WorkflowRunOperationHandler, ) from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -37,25 +42,29 @@ async def run(self, message: str) -> str: # Here we define a nexus service by providing a service handler implementation without a -# service contract. +# service contract. This nexus service has one operation. @service_handler class MyNexusServiceHandler: - # The nexus service has one operation. When using the workflow_run_operation_handler - # decorator, your start method must return a WorkflowHandle directly, using the - # temporalio.nexus.handler.start_workflow helper. (Temporal server takes care of - # delivering the workflow result to the caller, using the Nexus RPC callback mechanism). - @workflow_run_operation_handler - async def my_workflow_run_operation( - self, ctx: StartOperationContext, name: str - ) -> WorkflowOperationToken[str]: - tctx = TemporalNexusOperationContext.current() - return await tctx.start_workflow( - HandlerWorkflow.run, - name, - id=str(uuid.uuid4()), - client=tctx.client, - task_queue=tctx.task_queue, - ) + # Here we implement a Nexus operation backed by a Temporal workflow. The start + # method must use TemporalOperationContext.start_workflow to start the workflow, + # which returns a WorkflowOperationToken. (Temporal server will then take care of + # delivering the workflow result to the caller, using the Nexus RPC callback + # mechanism). + @operation_handler + def my_workflow_run_operation( + self, + ) -> OperationHandler[str, str]: + async def start( + ctx: StartOperationContext, name: str + ) -> WorkflowOperationToken[str]: + tctx = TemporalOperationContext.current() + return await tctx.start_workflow( + HandlerWorkflow.run, + name, + id=str(uuid.uuid4()), + ) + + return WorkflowRunOperationHandler(start) # From c986a8467d3735d5bf43827f5cf8c41478acba34 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 23 Jun 2025 16:15:09 -0400 Subject: [PATCH 15/46] uv.lock From f8643e75a21fd306cf4d6d5daf1ff42f7f133100 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Tue, 24 Jun 2025 12:15:23 -0400 Subject: [PATCH 16/46] Respond to upstream: temporal_operation_context --- hello_nexus/basic/handler/service_handler.py | 4 ++-- .../handler/service_handler_with_operation_handler_classes.py | 4 ++-- hello_nexus/without_service_definition/app.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index e2013119..00cdf01c 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -21,9 +21,9 @@ service_handler, ) from temporalio.nexus.handler import ( - TemporalOperationContext, WorkflowOperationToken, WorkflowRunOperationHandler, + temporal_operation_context, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -58,7 +58,7 @@ async def start( ctx: StartOperationContext, input: MyInput ) -> WorkflowOperationToken[MyOutput]: # You could use self.connected_db_client here. - tctx = TemporalOperationContext.current() + tctx = temporal_operation_context.get() return await tctx.start_workflow( WorkflowStartedByNexusOperation.run, input, diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index 4c3e872d..58c98394 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -33,8 +33,8 @@ class directly. service_handler, ) from temporalio.nexus.handler import ( - TemporalOperationContext, cancel_operation, + temporal_operation_context, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -130,7 +130,7 @@ class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): async def start( self, ctx: StartOperationContext, input: MyInput ) -> StartOperationResultAsync: - tctx = TemporalOperationContext.current() + tctx = temporal_operation_context.get() token = await tctx.start_workflow( WorkflowStartedByNexusOperation.run, input, diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index bf357f7d..9da26be1 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -18,9 +18,9 @@ from temporalio import workflow from temporalio.client import Client from temporalio.nexus.handler import ( - TemporalOperationContext, WorkflowOperationToken, WorkflowRunOperationHandler, + temporal_operation_context, ) from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -57,7 +57,7 @@ def my_workflow_run_operation( async def start( ctx: StartOperationContext, name: str ) -> WorkflowOperationToken[str]: - tctx = TemporalOperationContext.current() + tctx = temporal_operation_context.get() return await tctx.start_workflow( HandlerWorkflow.run, name, From b7a361ffc6d3dc49ea98dbed0145bb50ba01678a Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Tue, 24 Jun 2025 15:51:07 -0400 Subject: [PATCH 17/46] Respond to upstream: top-level start_workflow function --- hello_nexus/basic/handler/service_handler.py | 5 ++--- .../service_handler_with_operation_handler_classes.py | 5 ++--- hello_nexus/without_service_definition/app.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 00cdf01c..5af34b31 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -23,7 +23,7 @@ from temporalio.nexus.handler import ( WorkflowOperationToken, WorkflowRunOperationHandler, - temporal_operation_context, + start_workflow, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -58,8 +58,7 @@ async def start( ctx: StartOperationContext, input: MyInput ) -> WorkflowOperationToken[MyOutput]: # You could use self.connected_db_client here. - tctx = temporal_operation_context.get() - return await tctx.start_workflow( + return await start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index 58c98394..92b78cc7 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -34,7 +34,7 @@ class directly. ) from temporalio.nexus.handler import ( cancel_operation, - temporal_operation_context, + start_workflow, ) from hello_nexus.basic.handler.db_client import MyDBClient @@ -130,8 +130,7 @@ class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): async def start( self, ctx: StartOperationContext, input: MyInput ) -> StartOperationResultAsync: - tctx = temporal_operation_context.get() - token = await tctx.start_workflow( + token = await start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 9da26be1..017a35fa 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -20,7 +20,7 @@ from temporalio.nexus.handler import ( WorkflowOperationToken, WorkflowRunOperationHandler, - temporal_operation_context, + start_workflow, ) from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -57,8 +57,7 @@ def my_workflow_run_operation( async def start( ctx: StartOperationContext, name: str ) -> WorkflowOperationToken[str]: - tctx = temporal_operation_context.get() - return await tctx.start_workflow( + return await start_workflow( HandlerWorkflow.run, name, id=str(uuid.uuid4()), From df4794a3c45ffb817a69bad7370b108635f676a2 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 25 Jun 2025 09:09:09 -0400 Subject: [PATCH 18/46] Respond to upstream: from_callable, inherit from abstract base class --- hello_nexus/basic/handler/service_handler.py | 4 +- ..._handler_with_operation_handler_classes.py | 63 ++----------------- hello_nexus/without_service_definition/app.py | 2 +- 3 files changed, 8 insertions(+), 61 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 5af34b31..3515fcab 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -64,7 +64,7 @@ async def start( id=str(uuid.uuid4()), ) - return WorkflowRunOperationHandler(start) + return WorkflowRunOperationHandler.from_callable(start) # This is a sync operation. That means that unlike the workflow run operation above, # in this case the `start` method returns the final operation result. Sync operations @@ -78,4 +78,4 @@ async def start(ctx: StartOperationContext, input: MyInput) -> MyOutput: # You could use self.connected_db_client here. return MyOutput(message=f"Hello {input.name} from sync operation!") - return SyncOperationHandler(start) + return SyncOperationHandler.from_callable(start) diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index 92b78cc7..f9b28100 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -21,21 +21,15 @@ class directly. import uuid from nexusrpc.handler import ( - CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, OperationHandler, - OperationInfo, StartOperationContext, StartOperationResultAsync, StartOperationResultSync, + SyncOperationHandler, operation_handler, service_handler, ) -from temporalio.nexus.handler import ( - cancel_operation, - start_workflow, -) +from temporalio.nexus.handler import WorkflowRunOperationHandler, start_workflow from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.service_handler import MyInput, MyNexusService, MyOutput @@ -67,7 +61,7 @@ def my_workflow_run_operation( # This is a Nexus operation that responds synchronously to all requests. -class MySyncOperation(OperationHandler[MyInput, MyOutput]): +class MySyncOperation(SyncOperationHandler[MyInput, MyOutput]): # You can add an __init__ method taking any required arguments, since you are in # control of instantiating the OperationHandler inside the operation handler method # above decorated with @operation_handler. @@ -82,45 +76,16 @@ async def start( output = MyOutput(message=f"Hello {input.name} from sync operation!") return StartOperationResultSync(output) - async def fetch_info( - self, - ctx: FetchOperationInfoContext, - token: str, - ) -> OperationInfo: - raise NotImplementedError( - "fetch_info is not supported when a Nexus operation is called by a Temporal workflow" - ) - - async def fetch_result( - self, - ctx: FetchOperationResultContext, - token: str, - ) -> MyOutput: - raise NotImplementedError( - "fetch_result is not supported when a Nexus operation is called by a Temporal workflow, " - "but this sample does not demonstrate result fetching" - ) - - async def cancel( - self, - ctx: CancelOperationContext, - token: str, - ) -> None: - raise NotImplementedError( - "cancel is supported when a Nexus operation is called by a Temporal workflow, " - "but this sample does not demonstrate cancellation" - ) - # This is a Nexus operation that is backed by a Temporal workflow. That means that it # responds asynchronously to all requests: it starts a workflow and responds with a token # that the handler can associate with the worklow is started. -class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): +class MyWorkflowRunOperation(WorkflowRunOperationHandler[MyInput, MyOutput]): # You can add an __init__ method taking any required arguments, since you are in # control of instantiating the OperationHandler inside the operation handler method # above decorated with @operation_handler. - # The start method starts a workflow, and returns a WorkflowRunOperationResult that it + # The start method starts a workflow, and returns a StartOperationResultAsync that it # creates from the workflow handle. This return value contains the Nexus operation # token that the handler can use to obtain a handle and interact with the workflow on # future requests (for example if a cancel request is subsequently sent by the @@ -136,21 +101,3 @@ async def start( id=str(uuid.uuid4()), ) return StartOperationResultAsync(token.encode()) - - async def cancel(self, ctx: CancelOperationContext, token: str) -> None: - return await cancel_operation(token) - - async def fetch_info( - self, ctx: FetchOperationInfoContext, token: str - ) -> OperationInfo: - raise NotImplementedError( - "fetch_info is not supported when a Nexus operation is called by a Temporal workflow" - ) - - async def fetch_result( - self, ctx: FetchOperationResultContext, token: str - ) -> MyOutput: - raise NotImplementedError( - "fetch_result is not supported when a Nexus operation is called by a Temporal workflow, " - "but this sample does not demonstrate result fetching" - ) diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 017a35fa..8ba7f40b 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -63,7 +63,7 @@ async def start( id=str(uuid.uuid4()), ) - return WorkflowRunOperationHandler(start) + return WorkflowRunOperationHandler.from_callable(start) # From 3de98fc2b2e0dcacae4a70e830dbdc95f062043a Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 25 Jun 2025 09:18:46 -0400 Subject: [PATCH 19/46] Cleanup --- hello_nexus/basic/handler/service_handler.py | 21 +++++---- ..._handler_with_operation_handler_classes.py | 43 +++++++++++-------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 3515fcab..70c7b670 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -43,10 +43,9 @@ def __init__(self, connected_db_client: MyDBClient): self.connected_db_client = connected_db_client # This is a nexus operation that is backed by a Temporal workflow. The start method - # starts a workflow, and returns a nexus operation token synchronously. Meanwhile, - # the workflow executes in the background, and the Temporal server takes care of - # delivering the eventual workflow result (success or failure) to the calling - # workflow. + # starts a workflow, and returns a nexus operation token. Meanwhile, the workflow + # executes in the background; Temporal server takes care of delivering the eventual + # workflow result (success or failure) to the calling workflow. # # The token will be used by the caller if it subsequently wants to cancel the Nexus # operation. @@ -66,10 +65,16 @@ async def start( return WorkflowRunOperationHandler.from_callable(start) - # This is a sync operation. That means that unlike the workflow run operation above, - # in this case the `start` method returns the final operation result. Sync operations - # are free to make arbitrary network calls, or perform CPU-bound computations. Total - # execution duration must not exceed 10s. + # This is a Nexus operation that responds synchronously to all requests. That means + # that unlike the workflow run operation above, in this case the `start` method + # returns the final operation result. + # + # Here it is implemented using SyncOperationHandler.from_callable. + # See service_handler_with_operation_handler_classes.py for an alternative style + # involving subclassing SyncOperationHandler and overriding the start method. + # + # Sync operations are free to make arbitrary network calls, or perform CPU-bound + # computations. Total execution duration must not exceed 10s. @operation_handler def my_sync_operation( self, diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index f9b28100..194835eb 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -60,23 +60,6 @@ def my_workflow_run_operation( return MyWorkflowRunOperation() -# This is a Nexus operation that responds synchronously to all requests. -class MySyncOperation(SyncOperationHandler[MyInput, MyOutput]): - # You can add an __init__ method taking any required arguments, since you are in - # control of instantiating the OperationHandler inside the operation handler method - # above decorated with @operation_handler. - - # Unlike the workflow run operation below, the `start` method for a sync operation - # returns the final operation result. Sync operations are free to make arbitrary - # network calls, or perform CPU-bound computations. Total execution duration must not - # exceed 10s. async def start( - async def start( - self, ctx: StartOperationContext, input: MyInput - ) -> StartOperationResultSync[MyOutput]: - output = MyOutput(message=f"Hello {input.name} from sync operation!") - return StartOperationResultSync(output) - - # This is a Nexus operation that is backed by a Temporal workflow. That means that it # responds asynchronously to all requests: it starts a workflow and responds with a token # that the handler can associate with the worklow is started. @@ -101,3 +84,29 @@ async def start( id=str(uuid.uuid4()), ) return StartOperationResultAsync(token.encode()) + + +# This is a Nexus operation that responds synchronously to all requests. That means that +# unlike the workflow run operation above, in this case the `start` method returns the +# final operation result. +# +# Here it is implemented by subclassing SyncOperationHandler and overriding the start +# method. See service_handler.py for an alternative style using +# SyncOperationHandler.from_callable. +# +# Sync operations are free to make arbitrary network calls, or perform CPU-bound +# computations. Total execution duration must not exceed 10s. +class MySyncOperation(SyncOperationHandler[MyInput, MyOutput]): + # You can add an __init__ method taking any required arguments, since you are in + # control of instantiating the OperationHandler inside the operation handler method + # above decorated with @operation_handler. + + # Unlike the workflow run operation below, the `start` method for a sync operation + # returns the final operation result. Sync operations are free to make arbitrary + # network calls, or perform CPU-bound computations. Total execution duration must not + # exceed 10s. async def start( + async def start( + self, ctx: StartOperationContext, input: MyInput + ) -> StartOperationResultSync[MyOutput]: + output = MyOutput(message=f"Hello {input.name} from sync operation!") + return StartOperationResultSync(output) From 66016e7a7aed377263c19ce1dd9462713e744829 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 25 Jun 2025 23:24:29 -0400 Subject: [PATCH 20/46] Respond to upstream --- hello_nexus/basic/handler/service_handler.py | 51 +++++++------------ ..._handler_with_operation_handler_classes.py | 41 ++++++++++++--- hello_nexus/without_service_definition/app.py | 33 ++++-------- tests/hello_nexus/helpers.py | 2 - 4 files changed, 65 insertions(+), 62 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 70c7b670..8d246550 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -14,17 +14,12 @@ import uuid from nexusrpc.handler import ( - OperationHandler, StartOperationContext, - SyncOperationHandler, - operation_handler, service_handler, + sync_operation_handler, ) -from temporalio.nexus.handler import ( - WorkflowOperationToken, - WorkflowRunOperationHandler, - start_workflow, -) +from temporalio import nexus +from temporalio.nexus import workflow_run_operation_handler from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation @@ -49,21 +44,16 @@ def __init__(self, connected_db_client: MyDBClient): # # The token will be used by the caller if it subsequently wants to cancel the Nexus # operation. - @operation_handler - def my_workflow_run_operation( - self, - ) -> OperationHandler[MyInput, MyOutput]: - async def start( - ctx: StartOperationContext, input: MyInput - ) -> WorkflowOperationToken[MyOutput]: - # You could use self.connected_db_client here. - return await start_workflow( - WorkflowStartedByNexusOperation.run, - input, - id=str(uuid.uuid4()), - ) - - return WorkflowRunOperationHandler.from_callable(start) + @workflow_run_operation_handler + async def my_workflow_run_operation( + self, ctx: StartOperationContext, input: MyInput + ) -> nexus.WorkflowHandle[MyOutput]: + # You could use self.connected_db_client here. + return await nexus.start_workflow( + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + ) # This is a Nexus operation that responds synchronously to all requests. That means # that unlike the workflow run operation above, in this case the `start` method @@ -75,12 +65,9 @@ async def start( # # Sync operations are free to make arbitrary network calls, or perform CPU-bound # computations. Total execution duration must not exceed 10s. - @operation_handler - def my_sync_operation( - self, - ) -> OperationHandler[MyInput, MyOutput]: - async def start(ctx: StartOperationContext, input: MyInput) -> MyOutput: - # You could use self.connected_db_client here. - return MyOutput(message=f"Hello {input.name} from sync operation!") - - return SyncOperationHandler.from_callable(start) + @sync_operation_handler + async def my_sync_operation( + self, ctx: StartOperationContext, input: MyInput + ) -> MyOutput: + # You could use self.connected_db_client here. + return MyOutput(message=f"Hello {input.name} from sync operation!") diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py index 194835eb..a3455fae 100644 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py @@ -20,16 +20,19 @@ class directly. import uuid +from nexusrpc import OperationInfo from nexusrpc.handler import ( + CancelOperationContext, + FetchOperationInfoContext, + FetchOperationResultContext, OperationHandler, StartOperationContext, StartOperationResultAsync, StartOperationResultSync, - SyncOperationHandler, operation_handler, service_handler, ) -from temporalio.nexus.handler import WorkflowRunOperationHandler, start_workflow +from temporalio import nexus from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.service_handler import MyInput, MyNexusService, MyOutput @@ -63,7 +66,7 @@ def my_workflow_run_operation( # This is a Nexus operation that is backed by a Temporal workflow. That means that it # responds asynchronously to all requests: it starts a workflow and responds with a token # that the handler can associate with the worklow is started. -class MyWorkflowRunOperation(WorkflowRunOperationHandler[MyInput, MyOutput]): +class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): # You can add an __init__ method taking any required arguments, since you are in # control of instantiating the OperationHandler inside the operation handler method # above decorated with @operation_handler. @@ -78,12 +81,25 @@ class MyWorkflowRunOperation(WorkflowRunOperationHandler[MyInput, MyOutput]): async def start( self, ctx: StartOperationContext, input: MyInput ) -> StartOperationResultAsync: - token = await start_workflow( + handle = await nexus.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), ) - return StartOperationResultAsync(token.encode()) + return StartOperationResultAsync(handle.to_token()) + + async def fetch_info( + self, ctx: FetchOperationInfoContext, input: MyInput + ) -> OperationInfo: + raise NotImplementedError + + async def cancel(self, ctx: CancelOperationContext, input: MyInput) -> None: + raise NotImplementedError + + async def fetch_result( + self, ctx: FetchOperationResultContext, input: MyInput + ) -> MyOutput: + raise NotImplementedError # This is a Nexus operation that responds synchronously to all requests. That means that @@ -96,7 +112,7 @@ async def start( # # Sync operations are free to make arbitrary network calls, or perform CPU-bound # computations. Total execution duration must not exceed 10s. -class MySyncOperation(SyncOperationHandler[MyInput, MyOutput]): +class MySyncOperation(OperationHandler[MyInput, MyOutput]): # You can add an __init__ method taking any required arguments, since you are in # control of instantiating the OperationHandler inside the operation handler method # above decorated with @operation_handler. @@ -110,3 +126,16 @@ async def start( ) -> StartOperationResultSync[MyOutput]: output = MyOutput(message=f"Hello {input.name} from sync operation!") return StartOperationResultSync(output) + + async def fetch_info( + self, ctx: FetchOperationInfoContext, input: MyInput + ) -> OperationInfo: + raise NotImplementedError + + async def cancel(self, ctx: CancelOperationContext, input: MyInput) -> None: + raise NotImplementedError + + async def fetch_result( + self, ctx: FetchOperationResultContext, input: MyInput + ) -> MyOutput: + raise NotImplementedError diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 8ba7f40b..fc7b933f 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -10,18 +10,12 @@ from typing import Optional from nexusrpc.handler import ( - OperationHandler, StartOperationContext, - operation_handler, service_handler, ) -from temporalio import workflow +from temporalio import nexus, workflow from temporalio.client import Client -from temporalio.nexus.handler import ( - WorkflowOperationToken, - WorkflowRunOperationHandler, - start_workflow, -) +from temporalio.nexus import workflow_run_operation_handler from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -50,20 +44,15 @@ class MyNexusServiceHandler: # which returns a WorkflowOperationToken. (Temporal server will then take care of # delivering the workflow result to the caller, using the Nexus RPC callback # mechanism). - @operation_handler - def my_workflow_run_operation( - self, - ) -> OperationHandler[str, str]: - async def start( - ctx: StartOperationContext, name: str - ) -> WorkflowOperationToken[str]: - return await start_workflow( - HandlerWorkflow.run, - name, - id=str(uuid.uuid4()), - ) - - return WorkflowRunOperationHandler.from_callable(start) + @workflow_run_operation_handler + async def my_workflow_run_operation( + self, ctx: StartOperationContext, name: str + ) -> nexus.WorkflowHandle[str]: + return await nexus.start_workflow( + HandlerWorkflow.run, + name, + id=str(uuid.uuid4()), + ) # diff --git a/tests/hello_nexus/helpers.py b/tests/hello_nexus/helpers.py index 99be41ef..dee8dc18 100644 --- a/tests/hello_nexus/helpers.py +++ b/tests/hello_nexus/helpers.py @@ -6,8 +6,6 @@ import temporalio.api.nexus.v1 import temporalio.api.operatorservice import temporalio.api.operatorservice.v1 -import temporalio.nexus -import temporalio.nexus.handler from temporalio.client import Client From 65f76a075e4c1f82ef9cb014be59cfa3757b2578 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 25 Jun 2025 23:27:37 -0400 Subject: [PATCH 21/46] Delete operations-as-classes sample --- ..._handler_with_operation_handler_classes.py | 141 ------------------ hello_nexus/basic/handler/worker.py | 22 +-- tests/hello_nexus/basic_test.py | 36 +++-- 3 files changed, 21 insertions(+), 178 deletions(-) delete mode 100644 hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py diff --git a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py b/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py deleted file mode 100644 index a3455fae..00000000 --- a/hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -This file demonstrates how to define operation handlers by implementing an OperationHandler -class directly. - -See hello_nexus/basic/handler/service_handler.py for the alternative "shorthand" style -where you implement the `start` method only. - -Sync operations: ---------------- -Implementations are free to make arbitrary network calls, or perform CPU-bound -computations such as this one. Total execution duration must not exceed 10s. - - -Workflow operations: ---------------------- -The task queue defaults to the task queue being used by the Nexus worker. -""" - -from __future__ import annotations - -import uuid - -from nexusrpc import OperationInfo -from nexusrpc.handler import ( - CancelOperationContext, - FetchOperationInfoContext, - FetchOperationResultContext, - OperationHandler, - StartOperationContext, - StartOperationResultAsync, - StartOperationResultSync, - operation_handler, - service_handler, -) -from temporalio import nexus - -from hello_nexus.basic.handler.db_client import MyDBClient -from hello_nexus.basic.handler.service_handler import MyInput, MyNexusService, MyOutput -from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation - - -@service_handler(service=MyNexusService) -class MyNexusServiceHandlerUsingOperationHandlerClasses: - # You can create an __init__ method accepting what is needed by your operation - # handlers to handle requests. You typically instantiate your service handler class - # when starting your worker. See hello_nexus/basic/handler/worker.py. - def __init__(self, connected_db_client: MyDBClient): - # `connected_db_client` is intended as an example of something that might be - # required by your operation handlers when handling requests, but is only - # available at worker-start time. - self.connected_db_client = connected_db_client - - @operation_handler - def my_sync_operation(self) -> OperationHandler[MyInput, MyOutput]: - # Pass any required arguments to the OperationHandler __init__ method here. - return MySyncOperation() - - @operation_handler - def my_workflow_run_operation( - self, - ) -> OperationHandler[MyInput, MyOutput]: - # Pass any required arguments to the OperationHandler __init__ method here. - return MyWorkflowRunOperation() - - -# This is a Nexus operation that is backed by a Temporal workflow. That means that it -# responds asynchronously to all requests: it starts a workflow and responds with a token -# that the handler can associate with the worklow is started. -class MyWorkflowRunOperation(OperationHandler[MyInput, MyOutput]): - # You can add an __init__ method taking any required arguments, since you are in - # control of instantiating the OperationHandler inside the operation handler method - # above decorated with @operation_handler. - - # The start method starts a workflow, and returns a StartOperationResultAsync that it - # creates from the workflow handle. This return value contains the Nexus operation - # token that the handler can use to obtain a handle and interact with the workflow on - # future requests (for example if a cancel request is subsequently sent by the - # caller). The Temporal server takes care of delivering the workflow result to the - # calling workflow. The task queue defaults to the task queue being used by the Nexus - # worker. - async def start( - self, ctx: StartOperationContext, input: MyInput - ) -> StartOperationResultAsync: - handle = await nexus.start_workflow( - WorkflowStartedByNexusOperation.run, - input, - id=str(uuid.uuid4()), - ) - return StartOperationResultAsync(handle.to_token()) - - async def fetch_info( - self, ctx: FetchOperationInfoContext, input: MyInput - ) -> OperationInfo: - raise NotImplementedError - - async def cancel(self, ctx: CancelOperationContext, input: MyInput) -> None: - raise NotImplementedError - - async def fetch_result( - self, ctx: FetchOperationResultContext, input: MyInput - ) -> MyOutput: - raise NotImplementedError - - -# This is a Nexus operation that responds synchronously to all requests. That means that -# unlike the workflow run operation above, in this case the `start` method returns the -# final operation result. -# -# Here it is implemented by subclassing SyncOperationHandler and overriding the start -# method. See service_handler.py for an alternative style using -# SyncOperationHandler.from_callable. -# -# Sync operations are free to make arbitrary network calls, or perform CPU-bound -# computations. Total execution duration must not exceed 10s. -class MySyncOperation(OperationHandler[MyInput, MyOutput]): - # You can add an __init__ method taking any required arguments, since you are in - # control of instantiating the OperationHandler inside the operation handler method - # above decorated with @operation_handler. - - # Unlike the workflow run operation below, the `start` method for a sync operation - # returns the final operation result. Sync operations are free to make arbitrary - # network calls, or perform CPU-bound computations. Total execution duration must not - # exceed 10s. async def start( - async def start( - self, ctx: StartOperationContext, input: MyInput - ) -> StartOperationResultSync[MyOutput]: - output = MyOutput(message=f"Hello {input.name} from sync operation!") - return StartOperationResultSync(output) - - async def fetch_info( - self, ctx: FetchOperationInfoContext, input: MyInput - ) -> OperationInfo: - raise NotImplementedError - - async def cancel(self, ctx: CancelOperationContext, input: MyInput) -> None: - raise NotImplementedError - - async def fetch_result( - self, ctx: FetchOperationResultContext, input: MyInput - ) -> MyOutput: - raise NotImplementedError diff --git a/hello_nexus/basic/handler/worker.py b/hello_nexus/basic/handler/worker.py index 6fb273f6..788e5d5f 100644 --- a/hello_nexus/basic/handler/worker.py +++ b/hello_nexus/basic/handler/worker.py @@ -7,9 +7,6 @@ from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.service_handler import MyNexusServiceHandler -from hello_nexus.basic.handler.service_handler_with_operation_handler_classes import ( - MyNexusServiceHandlerUsingOperationHandlerClasses, -) from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation interrupt_event = asyncio.Event() @@ -18,12 +15,7 @@ TASK_QUEUE = "my-handler-task-queue" -async def main( - client: Optional[Client] = None, - # Change this to use the service handler defined in - # hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py - use_operation_handler_classes: bool = False, -): +async def main(client: Optional[Client] = None): logging.basicConfig(level=logging.INFO) client = client or await Client.connect( @@ -36,14 +28,6 @@ async def main( # requests. In this example we provide a database client object to the service hander. connected_db_client = MyDBClient.connect() - my_nexus_service_handler = ( - MyNexusServiceHandlerUsingOperationHandlerClasses( - connected_db_client=connected_db_client - ) - if use_operation_handler_classes - else MyNexusServiceHandler(connected_db_client=connected_db_client) - ) - # Start the worker, passing the Nexus service handler instance, in addition to the # workflow classes that are started by your nexus operations, and any activities # needed. This Worker will poll for both workflow tasks and Nexus tasks (this example @@ -52,7 +36,9 @@ async def main( client, task_queue=TASK_QUEUE, workflows=[WorkflowStartedByNexusOperation], - nexus_service_handlers=[my_nexus_service_handler], + nexus_service_handlers=[ + MyNexusServiceHandler(connected_db_client=connected_db_client) + ], ): logging.info("Worker started, ctrl+c to exit") await interrupt_event.wait() diff --git a/tests/hello_nexus/basic_test.py b/tests/hello_nexus/basic_test.py index e66e0b7f..d8fb2022 100644 --- a/tests/hello_nexus/basic_test.py +++ b/tests/hello_nexus/basic_test.py @@ -16,27 +16,25 @@ async def test_nexus_service_basic(client: Client): client=client, ) try: - for use_operation_handler_classes in [True, False]: - handler_worker_task = asyncio.create_task( - hello_nexus.basic.handler.worker.main( - client, - use_operation_handler_classes=use_operation_handler_classes, - ) - ) - await asyncio.sleep(1) - results = await hello_nexus.basic.caller.app.execute_caller_workflow( + handler_worker_task = asyncio.create_task( + hello_nexus.basic.handler.worker.main( client, ) - hello_nexus.basic.handler.worker.interrupt_event.set() - await handler_worker_task - hello_nexus.basic.handler.worker.interrupt_event.clear() - print("\n\n") - print([r.message for r in results]) - print("\n\n") - assert [r.message for r in results] == [ - "Hello world from sync operation!", - "Hello world from workflow run operation!", - ] + ) + await asyncio.sleep(1) + results = await hello_nexus.basic.caller.app.execute_caller_workflow( + client, + ) + hello_nexus.basic.handler.worker.interrupt_event.set() + await handler_worker_task + hello_nexus.basic.handler.worker.interrupt_event.clear() + print("\n\n") + print([r.message for r in results]) + print("\n\n") + assert [r.message for r in results] == [ + "Hello world from sync operation!", + "Hello world from workflow run operation!", + ] finally: await delete_nexus_endpoint( id=create_response.endpoint.id, From 1ed070cec20ef97e1e9aef0a3c7b10c8203000d4 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 06:48:08 -0400 Subject: [PATCH 22/46] Respond to upstream: rename decorators --- hello_nexus/basic/handler/service_handler.py | 12 ++++-------- hello_nexus/without_service_definition/app.py | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 8d246550..19a04f92 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -13,13 +13,9 @@ import uuid -from nexusrpc.handler import ( - StartOperationContext, - service_handler, - sync_operation_handler, -) +from nexusrpc.handler import StartOperationContext, service_handler, sync_operation from temporalio import nexus -from temporalio.nexus import workflow_run_operation_handler +from temporalio.nexus import workflow_run_operation from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation @@ -44,7 +40,7 @@ def __init__(self, connected_db_client: MyDBClient): # # The token will be used by the caller if it subsequently wants to cancel the Nexus # operation. - @workflow_run_operation_handler + @workflow_run_operation async def my_workflow_run_operation( self, ctx: StartOperationContext, input: MyInput ) -> nexus.WorkflowHandle[MyOutput]: @@ -65,7 +61,7 @@ async def my_workflow_run_operation( # # Sync operations are free to make arbitrary network calls, or perform CPU-bound # computations. Total execution duration must not exceed 10s. - @sync_operation_handler + @sync_operation async def my_sync_operation( self, ctx: StartOperationContext, input: MyInput ) -> MyOutput: diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index fc7b933f..872d1128 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -15,7 +15,7 @@ ) from temporalio import nexus, workflow from temporalio.client import Client -from temporalio.nexus import workflow_run_operation_handler +from temporalio.nexus import workflow_run_operation from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -44,7 +44,7 @@ class MyNexusServiceHandler: # which returns a WorkflowOperationToken. (Temporal server will then take care of # delivering the workflow result to the caller, using the Nexus RPC callback # mechanism). - @workflow_run_operation_handler + @workflow_run_operation async def my_workflow_run_operation( self, ctx: StartOperationContext, name: str ) -> nexus.WorkflowHandle[str]: From cd7e16d2e36e7bf6ddb3e122039cd05fdb9a56a7 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 07:02:03 -0400 Subject: [PATCH 23/46] Rename service class --- hello_nexus/without_service_definition/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 872d1128..15077c35 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -38,7 +38,7 @@ async def run(self, message: str) -> str: # Here we define a nexus service by providing a service handler implementation without a # service contract. This nexus service has one operation. @service_handler -class MyNexusServiceHandler: +class MyNexusService: # Here we implement a Nexus operation backed by a Temporal workflow. The start # method must use TemporalOperationContext.start_workflow to start the workflow, # which returns a WorkflowOperationToken. (Temporal server will then take care of @@ -70,9 +70,9 @@ async def run(self, message: str) -> str: # Normally, the first argument to both these calls would reference a service # contract class, but they can also reference your service handler class, as here. - nexus_client = NexusClient(MyNexusServiceHandler, endpoint=NEXUS_ENDPOINT) + nexus_client = NexusClient(MyNexusService, endpoint=NEXUS_ENDPOINT) return await nexus_client.execute_operation( - MyNexusServiceHandler.my_workflow_run_operation, message + MyNexusService.my_workflow_run_operation, message ) @@ -84,7 +84,7 @@ async def execute_caller_workflow(client: Optional[Client] = None) -> str: client, task_queue=TASK_QUEUE, workflows=[CallerWorkflow, HandlerWorkflow], - nexus_service_handlers=[MyNexusServiceHandler()], + nexus_service_handlers=[MyNexusService()], # TODO(dan): isinstance(op, nexusrpc.contract.Operation) is failing under the # sandbox in temporalio/worker/_interceptor.py workflow_runner=UnsandboxedWorkflowRunner(), From 4c1081695e889d492b6aa65394b1f9c6381ba0ec Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 07:29:46 -0400 Subject: [PATCH 24/46] Cleanup --- hello_nexus/basic/handler/service_handler.py | 13 +------------ hello_nexus/basic/handler/workflows.py | 5 +---- hello_nexus/without_service_definition/app.py | 7 +++---- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 19a04f92..391b25f1 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -1,12 +1,5 @@ """ -This file demonstrates how to define operation handlers by using a "shorthand" style in -which you implement the `start` method only. WorkflowRunOperationHandler implements -`cancel` for you automatically, but apart from that, the other operation methods -(`fetch_info`, `fetch_result`, and `cancel` for SyncOperationHandler) are all -automatically created with "raise NotImplementedError" implementations. - -See hello_nexus/basic/handler/service_handler_with_operation_handler_classes.py for the -alternative "fully manual" style where you implement an OperationHandler class directly. +This file demonstrates how to implement a Nexus service. """ from __future__ import annotations @@ -55,10 +48,6 @@ async def my_workflow_run_operation( # that unlike the workflow run operation above, in this case the `start` method # returns the final operation result. # - # Here it is implemented using SyncOperationHandler.from_callable. - # See service_handler_with_operation_handler_classes.py for an alternative style - # involving subclassing SyncOperationHandler and overriding the start method. - # # Sync operations are free to make arbitrary network calls, or perform CPU-bound # computations. Total execution duration must not exceed 10s. @sync_operation diff --git a/hello_nexus/basic/handler/workflows.py b/hello_nexus/basic/handler/workflows.py index e21e6aaa..bea1e975 100644 --- a/hello_nexus/basic/handler/workflows.py +++ b/hello_nexus/basic/handler/workflows.py @@ -3,12 +3,9 @@ from hello_nexus.basic.service import MyInput, MyOutput +# This is the workflow that is started by the `my_workflow_run_operation` nexus operation. @workflow.defn class WorkflowStartedByNexusOperation: - """ - This is the workflow that is started by the `my_workflow_run_operation` nexus operation. - """ - @workflow.run async def run(self, input: MyInput) -> MyOutput: return MyOutput(message=f"Hello {input.name} from workflow run operation!") diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 15077c35..71c91748 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -40,10 +40,9 @@ async def run(self, message: str) -> str: @service_handler class MyNexusService: # Here we implement a Nexus operation backed by a Temporal workflow. The start - # method must use TemporalOperationContext.start_workflow to start the workflow, - # which returns a WorkflowOperationToken. (Temporal server will then take care of - # delivering the workflow result to the caller, using the Nexus RPC callback - # mechanism). + # method must use nexus.start_workflow to start the workflow. (Temporal server will + # then take care of delivering the workflow result to the caller, using the Nexus + # RPC callback mechanism). @workflow_run_operation async def my_workflow_run_operation( self, ctx: StartOperationContext, name: str From fe3f23ea4f03d3542499b367415b6841598a009c Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 12:17:09 -0400 Subject: [PATCH 25/46] RTU: dependencies From 4513de4fa8e2ad2a481edcfc9c97a7876d9375ec Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 12:21:35 -0400 Subject: [PATCH 26/46] Pass through imports --- hello_nexus/basic/caller/workflows.py | 3 ++- hello_nexus/basic/handler/workflows.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/hello_nexus/basic/caller/workflows.py b/hello_nexus/basic/caller/workflows.py index 9c5f8fc9..732a6e11 100644 --- a/hello_nexus/basic/caller/workflows.py +++ b/hello_nexus/basic/caller/workflows.py @@ -1,7 +1,8 @@ from temporalio import workflow from temporalio.workflow import NexusClient -from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput +with workflow.unsafe.imports_passed_through(): + from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput NEXUS_ENDPOINT = "my-nexus-endpoint" diff --git a/hello_nexus/basic/handler/workflows.py b/hello_nexus/basic/handler/workflows.py index bea1e975..7f1ec3c2 100644 --- a/hello_nexus/basic/handler/workflows.py +++ b/hello_nexus/basic/handler/workflows.py @@ -1,6 +1,7 @@ from temporalio import workflow -from hello_nexus.basic.service import MyInput, MyOutput +with workflow.unsafe.imports_passed_through(): + from hello_nexus.basic.service import MyInput, MyOutput # This is the workflow that is started by the `my_workflow_run_operation` nexus operation. From 25da5a04e4ca96100be4441e555b408390282474 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 12:28:01 -0400 Subject: [PATCH 27/46] Fix directory paths in README --- hello_nexus/basic/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hello_nexus/basic/README.md b/hello_nexus/basic/README.md index 32f2de98..2d1f77e7 100644 --- a/hello_nexus/basic/README.md +++ b/hello_nexus/basic/README.md @@ -10,7 +10,7 @@ call the operations from a workflow. ### Instructions -Start a Temporal server. +Start a Temporal server. (See the main samples repo [README](../../README.md)). Run the following: @@ -22,16 +22,16 @@ temporal operator nexus endpoint create \ --name my-nexus-endpoint \ --target-namespace my-handler-namespace \ --target-task-queue my-handler-task-queue \ - --description-file ./hello_nexus/basic/endpoint_description.md + --description-file endpoint_description.md ``` -In one terminal, run the Temporal worker in the handler namespace: +In one terminal, in this directory, run the Temporal worker in the handler namespace: ``` -uv run hello_nexus/basic/handler/worker.py +uv run handler/worker.py ``` -In another terminal, run the Temporal worker in the caller namespace and start the caller +In another terminal, in this directory, run the Temporal worker in the caller namespace and start the caller workflow: ``` -uv run hello_nexus/basic/caller/app.py +uv run caller/app.py ``` From 25657639cfe52182a80135fd88ac19e80468eedc Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 12:37:43 -0400 Subject: [PATCH 28/46] Make namespace/task queue/enspoint names sample-specific --- hello_nexus/basic/README.md | 8 ++++---- hello_nexus/basic/caller/app.py | 4 ++-- hello_nexus/basic/caller/workflows.py | 2 +- hello_nexus/basic/handler/worker.py | 2 +- hello_nexus/without_service_definition/README.md | 2 +- hello_nexus/without_service_definition/app.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hello_nexus/basic/README.md b/hello_nexus/basic/README.md index 2d1f77e7..b795482a 100644 --- a/hello_nexus/basic/README.md +++ b/hello_nexus/basic/README.md @@ -15,12 +15,12 @@ Start a Temporal server. (See the main samples repo [README](../../README.md)). Run the following: ``` -temporal operator namespace create --namespace my-handler-namespace -temporal operator namespace create --namespace my-caller-namespace +temporal operator namespace create --namespace hello-nexus-basic-handler-namespace +temporal operator namespace create --namespace hello-nexus-basic-caller-namespace temporal operator nexus endpoint create \ - --name my-nexus-endpoint \ - --target-namespace my-handler-namespace \ + --name hello-nexus-basic-nexus-endpoint \ + --target-namespace hello-nexus-basic-handler-namespace \ --target-task-queue my-handler-task-queue \ --description-file endpoint_description.md ``` diff --git a/hello_nexus/basic/caller/app.py b/hello_nexus/basic/caller/app.py index 3e1f95ad..03296614 100644 --- a/hello_nexus/basic/caller/app.py +++ b/hello_nexus/basic/caller/app.py @@ -8,8 +8,8 @@ from hello_nexus.basic.caller.workflows import CallerWorkflow from hello_nexus.basic.service import MyOutput -NAMESPACE = "my-caller-namespace" -TASK_QUEUE = "my-caller-task-queue" +NAMESPACE = "hello-nexus-basic-caller-namespace" +TASK_QUEUE = "hello-nexus-basic-caller-task-queue" async def execute_caller_workflow( diff --git a/hello_nexus/basic/caller/workflows.py b/hello_nexus/basic/caller/workflows.py index 732a6e11..cdc76249 100644 --- a/hello_nexus/basic/caller/workflows.py +++ b/hello_nexus/basic/caller/workflows.py @@ -4,7 +4,7 @@ with workflow.unsafe.imports_passed_through(): from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput -NEXUS_ENDPOINT = "my-nexus-endpoint" +NEXUS_ENDPOINT = "hello-nexus-basic-nexus-endpoint" # This is a workflow that calls a nexus operation. diff --git a/hello_nexus/basic/handler/worker.py b/hello_nexus/basic/handler/worker.py index 788e5d5f..1632cd42 100644 --- a/hello_nexus/basic/handler/worker.py +++ b/hello_nexus/basic/handler/worker.py @@ -11,7 +11,7 @@ interrupt_event = asyncio.Event() -NAMESPACE = "my-handler-namespace" +NAMESPACE = "hello-nexus-basic-handler-namespace" TASK_QUEUE = "my-handler-task-queue" diff --git a/hello_nexus/without_service_definition/README.md b/hello_nexus/without_service_definition/README.md index 37ccdc97..bbfcf68c 100644 --- a/hello_nexus/without_service_definition/README.md +++ b/hello_nexus/without_service_definition/README.md @@ -14,7 +14,7 @@ Run the following: ``` temporal operator namespace create --namespace my-namespace temporal operator nexus endpoint create \ - --name my-nexus-endpoint \ + --name hello-nexus-basic-nexus-endpoint \ --handler-namespace my-namespace \ --handler-task-queue my-task-queue ``` diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index 71c91748..d727456d 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -21,7 +21,7 @@ NAMESPACE = "my-namespace" TASK_QUEUE = "my-task-queue" -NEXUS_ENDPOINT = "my-nexus-endpoint" +NEXUS_ENDPOINT = "hello-nexus-basic-nexus-endpoint" # # Handler From 6667d5e6ba7a8dd0be8a28f16524035771bcd4f6 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 15:27:12 -0400 Subject: [PATCH 29/46] RTU: ctx.start_workflow() --- hello_nexus/basic/handler/service_handler.py | 6 +++--- hello_nexus/without_service_definition/app.py | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/basic/handler/service_handler.py index 391b25f1..47e98a58 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/basic/handler/service_handler.py @@ -8,7 +8,7 @@ from nexusrpc.handler import StartOperationContext, service_handler, sync_operation from temporalio import nexus -from temporalio.nexus import workflow_run_operation +from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation from hello_nexus.basic.handler.db_client import MyDBClient from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation @@ -35,10 +35,10 @@ def __init__(self, connected_db_client: MyDBClient): # operation. @workflow_run_operation async def my_workflow_run_operation( - self, ctx: StartOperationContext, input: MyInput + self, ctx: WorkflowRunOperationContext, input: MyInput ) -> nexus.WorkflowHandle[MyOutput]: # You could use self.connected_db_client here. - return await nexus.start_workflow( + return await ctx.start_workflow( WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()), diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py index d727456d..d0ea90d4 100644 --- a/hello_nexus/without_service_definition/app.py +++ b/hello_nexus/without_service_definition/app.py @@ -10,12 +10,11 @@ from typing import Optional from nexusrpc.handler import ( - StartOperationContext, service_handler, ) from temporalio import nexus, workflow from temporalio.client import Client -from temporalio.nexus import workflow_run_operation +from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation from temporalio.worker import UnsandboxedWorkflowRunner, Worker from temporalio.workflow import NexusClient @@ -40,14 +39,14 @@ async def run(self, message: str) -> str: @service_handler class MyNexusService: # Here we implement a Nexus operation backed by a Temporal workflow. The start - # method must use nexus.start_workflow to start the workflow. (Temporal server will - # then take care of delivering the workflow result to the caller, using the Nexus - # RPC callback mechanism). + # method must use WorkflowRunOperationContext.start_workflow to start the workflow. + # (Temporal server will then take care of delivering the workflow result to the + # caller, using the Nexus RPC callback mechanism). @workflow_run_operation async def my_workflow_run_operation( - self, ctx: StartOperationContext, name: str + self, ctx: WorkflowRunOperationContext, name: str ) -> nexus.WorkflowHandle[str]: - return await nexus.start_workflow( + return await ctx.start_workflow( HandlerWorkflow.run, name, id=str(uuid.uuid4()), From 1ae914a97bb015403a38285001d86628b16ee1a9 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 15:34:43 -0400 Subject: [PATCH 30/46] Delete no-service-definition sample --- hello_nexus/README.md | 1 - .../without_service_definition/README.md | 25 ----- .../without_service_definition/__init__.py | 0 hello_nexus/without_service_definition/app.py | 104 ------------------ tests/hello_nexus/basic_test.py | 1 - .../without_workflow_service_test.py | 28 ----- 6 files changed, 159 deletions(-) delete mode 100644 hello_nexus/without_service_definition/README.md delete mode 100644 hello_nexus/without_service_definition/__init__.py delete mode 100644 hello_nexus/without_service_definition/app.py delete mode 100644 tests/hello_nexus/without_workflow_service_test.py diff --git a/hello_nexus/README.md b/hello_nexus/README.md index dafd1205..ed844a9c 100644 --- a/hello_nexus/README.md +++ b/hello_nexus/README.md @@ -12,4 +12,3 @@ The samples in this directory form an introduction to Nexus. ### Samples - [basic](./basic) - Nexus service definition, operation handlers, and calling workflows. -- [without_service_definition](./without_service_definition) - A Nexus service implementation without a service definition \ No newline at end of file diff --git a/hello_nexus/without_service_definition/README.md b/hello_nexus/without_service_definition/README.md deleted file mode 100644 index bbfcf68c..00000000 --- a/hello_nexus/without_service_definition/README.md +++ /dev/null @@ -1,25 +0,0 @@ -Usually you will want to create a service definition to formalize the service contract. -However it is possible to define a Nexus service and operation handlers without creating a -service definition. This sample demonstrates how to do that. This may be appropriate if -you want to call a Nexus operation that is being executed by a Worker in the same -namespace as the caller: in other words, if the Nexus operation is playing a role similar -to an Activity. - -### Instructions - -Start a Temporal server. - -Run the following: - -``` -temporal operator namespace create --namespace my-namespace -temporal operator nexus endpoint create \ - --name hello-nexus-basic-nexus-endpoint \ - --handler-namespace my-namespace \ - --handler-task-queue my-task-queue -``` - -From the root of the repo, run -``` -uv run hello_nexus/without_service_definition/app.py -``` diff --git a/hello_nexus/without_service_definition/__init__.py b/hello_nexus/without_service_definition/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/hello_nexus/without_service_definition/app.py b/hello_nexus/without_service_definition/app.py deleted file mode 100644 index d0ea90d4..00000000 --- a/hello_nexus/without_service_definition/app.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -This file demonstrates running a nexus service in the same namespace as the workflow that -is calling the nexus service, without specifying a separate service definition. -""" - -from __future__ import annotations - -import asyncio -import uuid -from typing import Optional - -from nexusrpc.handler import ( - service_handler, -) -from temporalio import nexus, workflow -from temporalio.client import Client -from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation -from temporalio.worker import UnsandboxedWorkflowRunner, Worker -from temporalio.workflow import NexusClient - -NAMESPACE = "my-namespace" -TASK_QUEUE = "my-task-queue" -NEXUS_ENDPOINT = "hello-nexus-basic-nexus-endpoint" - -# -# Handler -# - - -@workflow.defn -class HandlerWorkflow: - @workflow.run - async def run(self, message: str) -> str: - return f"Hello {message} from workflow run operation!" - - -# Here we define a nexus service by providing a service handler implementation without a -# service contract. This nexus service has one operation. -@service_handler -class MyNexusService: - # Here we implement a Nexus operation backed by a Temporal workflow. The start - # method must use WorkflowRunOperationContext.start_workflow to start the workflow. - # (Temporal server will then take care of delivering the workflow result to the - # caller, using the Nexus RPC callback mechanism). - @workflow_run_operation - async def my_workflow_run_operation( - self, ctx: WorkflowRunOperationContext, name: str - ) -> nexus.WorkflowHandle[str]: - return await ctx.start_workflow( - HandlerWorkflow.run, - name, - id=str(uuid.uuid4()), - ) - - -# -# Caller -# - - -@workflow.defn -class CallerWorkflow: - @workflow.run - async def run(self, message: str) -> str: - # Create the type-safe workflow nexus service client, and invoke the nexus - # operation. - # - # Normally, the first argument to both these calls would reference a service - # contract class, but they can also reference your service handler class, as here. - - nexus_client = NexusClient(MyNexusService, endpoint=NEXUS_ENDPOINT) - return await nexus_client.execute_operation( - MyNexusService.my_workflow_run_operation, message - ) - - -async def execute_caller_workflow(client: Optional[Client] = None) -> str: - client = client or await Client.connect("localhost:7233", namespace=NAMESPACE) - # Start a worker that polls for tasks for both the caller workflow and the nexus - # service. - async with Worker( - client, - task_queue=TASK_QUEUE, - workflows=[CallerWorkflow, HandlerWorkflow], - nexus_service_handlers=[MyNexusService()], - # TODO(dan): isinstance(op, nexusrpc.contract.Operation) is failing under the - # sandbox in temporalio/worker/_interceptor.py - workflow_runner=UnsandboxedWorkflowRunner(), - ): - return await client.execute_workflow( - CallerWorkflow.run, - "world", - id=str(uuid.uuid4()), - task_queue=TASK_QUEUE, - ) - - -if __name__ == "__main__": - loop = asyncio.new_event_loop() - try: - result = loop.run_until_complete(execute_caller_workflow()) - print(result) - except KeyboardInterrupt: - loop.run_until_complete(loop.shutdown_asyncgens()) diff --git a/tests/hello_nexus/basic_test.py b/tests/hello_nexus/basic_test.py index d8fb2022..f2b3a122 100644 --- a/tests/hello_nexus/basic_test.py +++ b/tests/hello_nexus/basic_test.py @@ -5,7 +5,6 @@ import hello_nexus.basic.caller.app import hello_nexus.basic.caller.workflows import hello_nexus.basic.handler.worker -import hello_nexus.without_service_definition.app from tests.hello_nexus.helpers import create_nexus_endpoint, delete_nexus_endpoint diff --git a/tests/hello_nexus/without_workflow_service_test.py b/tests/hello_nexus/without_workflow_service_test.py deleted file mode 100644 index 4739dd8a..00000000 --- a/tests/hello_nexus/without_workflow_service_test.py +++ /dev/null @@ -1,28 +0,0 @@ -from temporalio.client import Client - -from hello_nexus.without_service_definition.app import ( - NEXUS_ENDPOINT, - TASK_QUEUE, - execute_caller_workflow, -) -from tests.hello_nexus.helpers import create_nexus_endpoint, delete_nexus_endpoint - - -# TODO(dan): This test is very slow (~10s) compared to tests/hello_nexus/basic_test.py. -# One difference is that in this test there is only one worker, polling for both workflow -# and nexus tasks. -async def test_nexus_service_without_service_definition(client: Client): - create_response = await create_nexus_endpoint( - name=NEXUS_ENDPOINT, - task_queue=TASK_QUEUE, - client=client, - ) - try: - result = await execute_caller_workflow(client) - assert result == "Hello world from workflow run operation!" - finally: - await delete_nexus_endpoint( - id=create_response.endpoint.id, - version=create_response.endpoint.version, - client=client, - ) From 96f7d1dca0e0cf33c19f81160fb12c93841b3e21 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 15:38:33 -0400 Subject: [PATCH 31/46] Reorganize --- hello_nexus/README.md | 41 +++++++++++++++---- hello_nexus/basic/README.md | 37 ----------------- hello_nexus/basic/handler/__init__.py | 0 hello_nexus/{basic => caller}/__init__.py | 0 hello_nexus/{basic => }/caller/app.py | 4 +- hello_nexus/{basic => }/caller/workflows.py | 2 +- .../{basic => }/endpoint_description.md | 0 .../{basic/caller => handler}/__init__.py | 0 hello_nexus/{basic => }/handler/db_client.py | 0 .../{basic => }/handler/service_handler.py | 6 +-- hello_nexus/{basic => }/handler/worker.py | 6 +-- hello_nexus/{basic => }/handler/workflows.py | 2 +- hello_nexus/{basic => }/service.py | 0 pyproject.toml | 2 +- .../{basic_test.py => hello_nexus_test.py} | 18 ++++---- 15 files changed, 52 insertions(+), 66 deletions(-) delete mode 100644 hello_nexus/basic/README.md delete mode 100644 hello_nexus/basic/handler/__init__.py rename hello_nexus/{basic => caller}/__init__.py (100%) rename hello_nexus/{basic => }/caller/app.py (92%) rename hello_nexus/{basic => }/caller/workflows.py (94%) rename hello_nexus/{basic => }/endpoint_description.md (100%) rename hello_nexus/{basic/caller => handler}/__init__.py (100%) rename hello_nexus/{basic => }/handler/db_client.py (100%) rename hello_nexus/{basic => }/handler/service_handler.py (91%) rename hello_nexus/{basic => }/handler/worker.py (88%) rename hello_nexus/{basic => }/handler/workflows.py (86%) rename hello_nexus/{basic => }/service.py (100%) rename tests/hello_nexus/{basic_test.py => hello_nexus_test.py} (64%) diff --git a/hello_nexus/README.md b/hello_nexus/README.md index ed844a9c..0177d868 100644 --- a/hello_nexus/README.md +++ b/hello_nexus/README.md @@ -1,14 +1,37 @@ -# Nexus +This sample shows how to define a Nexus service, implement the operation handlers, and +call the operations from a workflow. -Temporal Nexus is a feature of the Temporal platform designed to connect durable executions across team, namespace, -region, and cloud boundaries. It promotes a more modular architecture for sharing a subset of your team’s capabilities -via well-defined service API contracts for other teams to use. These can abstract underlying Temporal primitives such as -Workflows, or execute arbitrary code. +### Sample directory structure -Learn more at [temporal.io/nexus](https://temporal.io/nexus). +- [service.py](./service.py) - shared Nexus service definition +- [caller](./caller) - a caller workflow that executes Nexus operations, together with a worker and starter code +- [handler](./handler) - Nexus operation handlers, together with a workflow used by one of the Nexus operations, and a worker that polls for both workflow and Nexus tasks. -The samples in this directory form an introduction to Nexus. -### Samples +### Instructions -- [basic](./basic) - Nexus service definition, operation handlers, and calling workflows. +Start a Temporal server. (See the main samples repo [README](../README.md)). + +Run the following: + +``` +temporal operator namespace create --namespace hello-nexus-basic-handler-namespace +temporal operator namespace create --namespace hello-nexus-basic-caller-namespace + +temporal operator nexus endpoint create \ + --name hello-nexus-basic-nexus-endpoint \ + --target-namespace hello-nexus-basic-handler-namespace \ + --target-task-queue my-handler-task-queue \ + --description-file endpoint_description.md +``` + +In one terminal, in this directory, run the Temporal worker in the handler namespace: +``` +uv run handler/worker.py +``` + +In another terminal, in this directory, run the Temporal worker in the caller namespace and start the caller +workflow: +``` +uv run caller/app.py +``` diff --git a/hello_nexus/basic/README.md b/hello_nexus/basic/README.md deleted file mode 100644 index b795482a..00000000 --- a/hello_nexus/basic/README.md +++ /dev/null @@ -1,37 +0,0 @@ -This sample shows how to define a Nexus service, implement the operation handlers, and -call the operations from a workflow. - -### Sample directory structure - -- [service.py](./service.py) - shared Nexus service definition -- [caller](./caller) - a caller workflow that executes Nexus operations, together with a worker and starter code -- [handler](./handler) - Nexus operation handlers, together with a workflow used by one of the Nexus operations, and a worker that polls for both workflow and Nexus tasks. - - -### Instructions - -Start a Temporal server. (See the main samples repo [README](../../README.md)). - -Run the following: - -``` -temporal operator namespace create --namespace hello-nexus-basic-handler-namespace -temporal operator namespace create --namespace hello-nexus-basic-caller-namespace - -temporal operator nexus endpoint create \ - --name hello-nexus-basic-nexus-endpoint \ - --target-namespace hello-nexus-basic-handler-namespace \ - --target-task-queue my-handler-task-queue \ - --description-file endpoint_description.md -``` - -In one terminal, in this directory, run the Temporal worker in the handler namespace: -``` -uv run handler/worker.py -``` - -In another terminal, in this directory, run the Temporal worker in the caller namespace and start the caller -workflow: -``` -uv run caller/app.py -``` diff --git a/hello_nexus/basic/handler/__init__.py b/hello_nexus/basic/handler/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/hello_nexus/basic/__init__.py b/hello_nexus/caller/__init__.py similarity index 100% rename from hello_nexus/basic/__init__.py rename to hello_nexus/caller/__init__.py diff --git a/hello_nexus/basic/caller/app.py b/hello_nexus/caller/app.py similarity index 92% rename from hello_nexus/basic/caller/app.py rename to hello_nexus/caller/app.py index 03296614..6a656a2c 100644 --- a/hello_nexus/basic/caller/app.py +++ b/hello_nexus/caller/app.py @@ -5,8 +5,8 @@ from temporalio.client import Client from temporalio.worker import UnsandboxedWorkflowRunner, Worker -from hello_nexus.basic.caller.workflows import CallerWorkflow -from hello_nexus.basic.service import MyOutput +from hello_nexus.caller.workflows import CallerWorkflow +from hello_nexus.service import MyOutput NAMESPACE = "hello-nexus-basic-caller-namespace" TASK_QUEUE = "hello-nexus-basic-caller-task-queue" diff --git a/hello_nexus/basic/caller/workflows.py b/hello_nexus/caller/workflows.py similarity index 94% rename from hello_nexus/basic/caller/workflows.py rename to hello_nexus/caller/workflows.py index cdc76249..022eb8d9 100644 --- a/hello_nexus/basic/caller/workflows.py +++ b/hello_nexus/caller/workflows.py @@ -2,7 +2,7 @@ from temporalio.workflow import NexusClient with workflow.unsafe.imports_passed_through(): - from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput + from hello_nexus.service import MyInput, MyNexusService, MyOutput NEXUS_ENDPOINT = "hello-nexus-basic-nexus-endpoint" diff --git a/hello_nexus/basic/endpoint_description.md b/hello_nexus/endpoint_description.md similarity index 100% rename from hello_nexus/basic/endpoint_description.md rename to hello_nexus/endpoint_description.md diff --git a/hello_nexus/basic/caller/__init__.py b/hello_nexus/handler/__init__.py similarity index 100% rename from hello_nexus/basic/caller/__init__.py rename to hello_nexus/handler/__init__.py diff --git a/hello_nexus/basic/handler/db_client.py b/hello_nexus/handler/db_client.py similarity index 100% rename from hello_nexus/basic/handler/db_client.py rename to hello_nexus/handler/db_client.py diff --git a/hello_nexus/basic/handler/service_handler.py b/hello_nexus/handler/service_handler.py similarity index 91% rename from hello_nexus/basic/handler/service_handler.py rename to hello_nexus/handler/service_handler.py index 47e98a58..3a373691 100644 --- a/hello_nexus/basic/handler/service_handler.py +++ b/hello_nexus/handler/service_handler.py @@ -10,9 +10,9 @@ from temporalio import nexus from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation -from hello_nexus.basic.handler.db_client import MyDBClient -from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation -from hello_nexus.basic.service import MyInput, MyNexusService, MyOutput +from hello_nexus.handler.db_client import MyDBClient +from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation +from hello_nexus.service import MyInput, MyNexusService, MyOutput @service_handler(service=MyNexusService) diff --git a/hello_nexus/basic/handler/worker.py b/hello_nexus/handler/worker.py similarity index 88% rename from hello_nexus/basic/handler/worker.py rename to hello_nexus/handler/worker.py index 1632cd42..b982c542 100644 --- a/hello_nexus/basic/handler/worker.py +++ b/hello_nexus/handler/worker.py @@ -5,9 +5,9 @@ from temporalio.client import Client from temporalio.worker import Worker -from hello_nexus.basic.handler.db_client import MyDBClient -from hello_nexus.basic.handler.service_handler import MyNexusServiceHandler -from hello_nexus.basic.handler.workflows import WorkflowStartedByNexusOperation +from hello_nexus.handler.db_client import MyDBClient +from hello_nexus.handler.service_handler import MyNexusServiceHandler +from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation interrupt_event = asyncio.Event() diff --git a/hello_nexus/basic/handler/workflows.py b/hello_nexus/handler/workflows.py similarity index 86% rename from hello_nexus/basic/handler/workflows.py rename to hello_nexus/handler/workflows.py index 7f1ec3c2..a41b29ef 100644 --- a/hello_nexus/basic/handler/workflows.py +++ b/hello_nexus/handler/workflows.py @@ -1,7 +1,7 @@ from temporalio import workflow with workflow.unsafe.imports_passed_through(): - from hello_nexus.basic.service import MyInput, MyOutput + from hello_nexus.service import MyInput, MyOutput # This is the workflow that is started by the `my_workflow_run_operation` nexus operation. diff --git a/hello_nexus/basic/service.py b/hello_nexus/service.py similarity index 100% rename from hello_nexus/basic/service.py rename to hello_nexus/service.py diff --git a/pyproject.toml b/pyproject.toml index 6c0730b8..22caed65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,9 +106,9 @@ packages = [ "encryption", "gevent_async", "hello", - "hello_nexus", "langchain", "message_passing", + "nexus", "open_telemetry", "patching", "polling", diff --git a/tests/hello_nexus/basic_test.py b/tests/hello_nexus/hello_nexus_test.py similarity index 64% rename from tests/hello_nexus/basic_test.py rename to tests/hello_nexus/hello_nexus_test.py index f2b3a122..c8a5b673 100644 --- a/tests/hello_nexus/basic_test.py +++ b/tests/hello_nexus/hello_nexus_test.py @@ -2,31 +2,31 @@ from temporalio.client import Client -import hello_nexus.basic.caller.app -import hello_nexus.basic.caller.workflows -import hello_nexus.basic.handler.worker +import hello_nexus.caller.app +import hello_nexus.caller.workflows +import hello_nexus.handler.worker from tests.hello_nexus.helpers import create_nexus_endpoint, delete_nexus_endpoint async def test_nexus_service_basic(client: Client): create_response = await create_nexus_endpoint( - name=hello_nexus.basic.caller.workflows.NEXUS_ENDPOINT, - task_queue=hello_nexus.basic.handler.worker.TASK_QUEUE, + name=hello_nexus.caller.workflows.NEXUS_ENDPOINT, + task_queue=hello_nexus.handler.worker.TASK_QUEUE, client=client, ) try: handler_worker_task = asyncio.create_task( - hello_nexus.basic.handler.worker.main( + hello_nexus.handler.worker.main( client, ) ) await asyncio.sleep(1) - results = await hello_nexus.basic.caller.app.execute_caller_workflow( + results = await hello_nexus.caller.app.execute_caller_workflow( client, ) - hello_nexus.basic.handler.worker.interrupt_event.set() + hello_nexus.handler.worker.interrupt_event.set() await handler_worker_task - hello_nexus.basic.handler.worker.interrupt_event.clear() + hello_nexus.handler.worker.interrupt_event.clear() print("\n\n") print([r.message for r in results]) print("\n\n") From 2da62a07f2fa8537364e5ddb38d07103c96a3e4d Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 26 Jun 2025 17:14:21 -0400 Subject: [PATCH 32/46] Enable sandbox in Nexus sample --- hello_nexus/caller/app.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/hello_nexus/caller/app.py b/hello_nexus/caller/app.py index 6a656a2c..40785b90 100644 --- a/hello_nexus/caller/app.py +++ b/hello_nexus/caller/app.py @@ -3,7 +3,7 @@ from typing import Optional from temporalio.client import Client -from temporalio.worker import UnsandboxedWorkflowRunner, Worker +from temporalio.worker import Worker from hello_nexus.caller.workflows import CallerWorkflow from hello_nexus.service import MyOutput @@ -24,9 +24,6 @@ async def execute_caller_workflow( client, task_queue=TASK_QUEUE, workflows=[CallerWorkflow], - # TODO(dan): isinstance(op, nexusrpc.contract.Operation) is failing under the - # sandbox in temporalio/worker/_interceptor.py - workflow_runner=UnsandboxedWorkflowRunner(), ): return await client.execute_workflow( CallerWorkflow.run, From d4977c661d67c59cbe1f89c5313d637aee6905c4 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Fri, 27 Jun 2025 14:28:23 -0400 Subject: [PATCH 33/46] uv.lock From 4b4e520e3f7cd1b778591e884f39dc26e1798063 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 29 Jun 2025 22:21:25 -0400 Subject: [PATCH 34/46] uv.lock From acdf591a902ff41399d58a8062feab5f9965331d Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 6 Jul 2025 20:22:59 -0400 Subject: [PATCH 35/46] RTU: Nexus client --- hello_nexus/caller/workflows.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hello_nexus/caller/workflows.py b/hello_nexus/caller/workflows.py index 022eb8d9..29253ac4 100644 --- a/hello_nexus/caller/workflows.py +++ b/hello_nexus/caller/workflows.py @@ -1,5 +1,4 @@ from temporalio import workflow -from temporalio.workflow import NexusClient with workflow.unsafe.imports_passed_through(): from hello_nexus.service import MyInput, MyNexusService, MyOutput @@ -13,8 +12,8 @@ class CallerWorkflow: # An __init__ method is always optional on a Workflow class. Here we use it to set the # NexusClient, but that could alternatively be done in the run method. def __init__(self): - self.nexus_client = NexusClient( - MyNexusService, + self.nexus_client = workflow.create_nexus_client( + service=MyNexusService, endpoint=NEXUS_ENDPOINT, ) From 080b804bf873e7620e2718fb290e784201720f31 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 7 Jul 2025 10:47:39 -0400 Subject: [PATCH 36/46] Install SDKs from github --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22caed65..cd81f588 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,8 +86,8 @@ default-groups = [ ] [tool.uv.sources] -nexus-rpc = { path = "../nexus-sdk-python", editable = true } -temporalio = { path = "../sdk-python", editable = true } +nexus-rpc = { git = "https://github.com/nexus-rpc/sdk-python" } +temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "nexus" } [tool.hatch.build.targets.sdist] include = ["./**/*.py"] From c41fb80a84f712821be91646ea067927bf2c4a1d Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 7 Jul 2025 20:32:36 -0400 Subject: [PATCH 37/46] Don't demonstrate passing in a db client --- hello_nexus/handler/db_client.py | 23 ----------------------- hello_nexus/handler/service_handler.py | 8 -------- hello_nexus/handler/worker.py | 10 +--------- 3 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 hello_nexus/handler/db_client.py diff --git a/hello_nexus/handler/db_client.py b/hello_nexus/handler/db_client.py deleted file mode 100644 index fac38a1d..00000000 --- a/hello_nexus/handler/db_client.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations - - -class MyDBClient: - """ - This class represents a resource that your Nexus operation handlers may need when they - are handling Nexus requests, but which is only available when the Nexus worker is - started. Notice that: - - (a) The user's service handler class __init__ constructor takes a MyDBClient instance - (see hello_nexus.handler.MyNexusService) - - (b) The user is responsible for instantiating the service handler class when they - start the worker (see hello_nexus.handler.worker), so they can pass any - necessary resources (such as this database client) to the service handler. - """ - - @classmethod - def connect(cls) -> MyDBClient: - return cls() - - def execute(self, query: str) -> str: - return "query-result" diff --git a/hello_nexus/handler/service_handler.py b/hello_nexus/handler/service_handler.py index 3a373691..411eece8 100644 --- a/hello_nexus/handler/service_handler.py +++ b/hello_nexus/handler/service_handler.py @@ -10,7 +10,6 @@ from temporalio import nexus from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation -from hello_nexus.handler.db_client import MyDBClient from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation from hello_nexus.service import MyInput, MyNexusService, MyOutput @@ -20,11 +19,6 @@ class MyNexusServiceHandler: # You can create an __init__ method accepting what is needed by your operation # handlers to handle requests. You typically instantiate your service handler class # when starting your worker. See hello_nexus/basic/handler/worker.py. - def __init__(self, connected_db_client: MyDBClient): - # `connected_db_client` is intended as an example of something that might be - # required by your operation handlers when handling requests, but is only - # available at worker-start time. - self.connected_db_client = connected_db_client # This is a nexus operation that is backed by a Temporal workflow. The start method # starts a workflow, and returns a nexus operation token. Meanwhile, the workflow @@ -37,7 +31,6 @@ def __init__(self, connected_db_client: MyDBClient): async def my_workflow_run_operation( self, ctx: WorkflowRunOperationContext, input: MyInput ) -> nexus.WorkflowHandle[MyOutput]: - # You could use self.connected_db_client here. return await ctx.start_workflow( WorkflowStartedByNexusOperation.run, input, @@ -54,5 +47,4 @@ async def my_workflow_run_operation( async def my_sync_operation( self, ctx: StartOperationContext, input: MyInput ) -> MyOutput: - # You could use self.connected_db_client here. return MyOutput(message=f"Hello {input.name} from sync operation!") diff --git a/hello_nexus/handler/worker.py b/hello_nexus/handler/worker.py index b982c542..0bdd6c01 100644 --- a/hello_nexus/handler/worker.py +++ b/hello_nexus/handler/worker.py @@ -5,7 +5,6 @@ from temporalio.client import Client from temporalio.worker import Worker -from hello_nexus.handler.db_client import MyDBClient from hello_nexus.handler.service_handler import MyNexusServiceHandler from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation @@ -23,11 +22,6 @@ async def main(client: Optional[Client] = None): namespace=NAMESPACE, ) - # Create an instance of the service handler. Your service handler class __init__ can - # be written to accept any arguments that your operation handlers need when handling - # requests. In this example we provide a database client object to the service hander. - connected_db_client = MyDBClient.connect() - # Start the worker, passing the Nexus service handler instance, in addition to the # workflow classes that are started by your nexus operations, and any activities # needed. This Worker will poll for both workflow tasks and Nexus tasks (this example @@ -36,9 +30,7 @@ async def main(client: Optional[Client] = None): client, task_queue=TASK_QUEUE, workflows=[WorkflowStartedByNexusOperation], - nexus_service_handlers=[ - MyNexusServiceHandler(connected_db_client=connected_db_client) - ], + nexus_service_handlers=[MyNexusServiceHandler()], ): logging.info("Worker started, ctrl+c to exit") await interrupt_event.wait() From a38d93fa99e2c471bceac108ddd68db21a186daa Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 7 Jul 2025 21:39:34 -0400 Subject: [PATCH 38/46] Add nexus-rpc version constraint --- pyproject.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd81f588..6bdfa233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ langchain = [ "uvicorn[standard]>=0.24.0.post1,<0.25", ] nexus = [ - "nexus-rpc", + "nexus-rpc>=1.1.0,<2", ] open-telemetry = [ "temporalio[opentelemetry]", @@ -85,10 +85,6 @@ default-groups = [ "trio-async", ] -[tool.uv.sources] -nexus-rpc = { git = "https://github.com/nexus-rpc/sdk-python" } -temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "nexus" } - [tool.hatch.build.targets.sdist] include = ["./**/*.py"] @@ -161,3 +157,4 @@ ignore_errors = true [[tool.mypy.overrides]] module = "opentelemetry.*" ignore_errors = true + From c90e714d56decc010cc646c4c73ea4ccde104cc2 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 7 Jul 2025 21:41:11 -0400 Subject: [PATCH 39/46] Use SDK from github --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6bdfa233..339cfb33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,9 @@ default-groups = [ "trio-async", ] +[tool.uv.sources] +temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "nexus" } + [tool.hatch.build.targets.sdist] include = ["./**/*.py"] From d5a561093138daa7de79e4527cc4b5fa5560ae37 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 7 Jul 2025 22:01:30 -0400 Subject: [PATCH 40/46] Revert "Use SDK from github" This reverts commit 62e81ae5581cb964443a655a171d8340392d2375. --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 339cfb33..6bdfa233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,9 +85,6 @@ default-groups = [ "trio-async", ] -[tool.uv.sources] -temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "nexus" } - [tool.hatch.build.targets.sdist] include = ["./**/*.py"] From 97507bd2f9a07b05854065987ee049aae8d2ca26 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 9 Jul 2025 09:11:27 -0400 Subject: [PATCH 41/46] dependencies From 6444d39f578ef52ebaef5aa24a04577bbf0119f6 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 9 Jul 2025 09:13:52 -0400 Subject: [PATCH 42/46] Run commands from repo root dir --- hello_nexus/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hello_nexus/README.md b/hello_nexus/README.md index 0177d868..bf26ce47 100644 --- a/hello_nexus/README.md +++ b/hello_nexus/README.md @@ -22,16 +22,16 @@ temporal operator nexus endpoint create \ --name hello-nexus-basic-nexus-endpoint \ --target-namespace hello-nexus-basic-handler-namespace \ --target-task-queue my-handler-task-queue \ - --description-file endpoint_description.md + --description-file hello_nexus/endpoint_description.md ``` -In one terminal, in this directory, run the Temporal worker in the handler namespace: +In one terminal, run the Temporal worker in the handler namespace: ``` -uv run handler/worker.py +uv run hello_nexus/handler/worker.py ``` -In another terminal, in this directory, run the Temporal worker in the caller namespace and start the caller +In another terminal, run the Temporal worker in the caller namespace and start the caller workflow: ``` -uv run caller/app.py +uv run hello_nexus/caller/app.py ``` From 6d4a0c259ebf9605285bd9947898126c4934b2af Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 9 Jul 2025 09:25:18 -0400 Subject: [PATCH 43/46] Edits, imports --- hello_nexus/caller/workflows.py | 14 +++++++------- hello_nexus/handler/service_handler.py | 13 ++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/hello_nexus/caller/workflows.py b/hello_nexus/caller/workflows.py index 29253ac4..240b8b8c 100644 --- a/hello_nexus/caller/workflows.py +++ b/hello_nexus/caller/workflows.py @@ -6,27 +6,27 @@ NEXUS_ENDPOINT = "hello-nexus-basic-nexus-endpoint" -# This is a workflow that calls a nexus operation. +# This is a workflow that calls two nexus operations. @workflow.defn class CallerWorkflow: - # An __init__ method is always optional on a Workflow class. Here we use it to set the - # NexusClient, but that could alternatively be done in the run method. + # An __init__ method is always optional on a workflow class. Here we use it to set the + # nexus client, but that could alternatively be done in the run method. def __init__(self): self.nexus_client = workflow.create_nexus_client( service=MyNexusService, endpoint=NEXUS_ENDPOINT, ) - # The Wokflow run method invokes two Nexus operations. + # The workflow run method invokes two nexus operations. @workflow.run async def run(self, name: str) -> tuple[MyOutput, MyOutput]: - # Start the Nexus operation and wait for the result in one go, using execute_operation. + # Start the nexus operation and wait for the result in one go, using execute_operation. wf_result = await self.nexus_client.execute_operation( MyNexusService.my_workflow_run_operation, MyInput(name), ) - # We could use execute_operation for this one also, but here we demonstrate - # obtaining the operation handle and then using it to get the result. + # Alternatively, you can use start_operation to obtain the operation handle and + # then `await` the handle to obtain the result. sync_operation_handle = await self.nexus_client.start_operation( MyNexusService.my_sync_operation, MyInput(name), diff --git a/hello_nexus/handler/service_handler.py b/hello_nexus/handler/service_handler.py index 411eece8..1295abd1 100644 --- a/hello_nexus/handler/service_handler.py +++ b/hello_nexus/handler/service_handler.py @@ -6,15 +6,14 @@ import uuid -from nexusrpc.handler import StartOperationContext, service_handler, sync_operation +import nexusrpc from temporalio import nexus -from temporalio.nexus import WorkflowRunOperationContext, workflow_run_operation from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation from hello_nexus.service import MyInput, MyNexusService, MyOutput -@service_handler(service=MyNexusService) +@nexusrpc.handler.service_handler(service=MyNexusService) class MyNexusServiceHandler: # You can create an __init__ method accepting what is needed by your operation # handlers to handle requests. You typically instantiate your service handler class @@ -27,9 +26,9 @@ class MyNexusServiceHandler: # # The token will be used by the caller if it subsequently wants to cancel the Nexus # operation. - @workflow_run_operation + @nexus.workflow_run_operation async def my_workflow_run_operation( - self, ctx: WorkflowRunOperationContext, input: MyInput + self, ctx: nexus.WorkflowRunOperationContext, input: MyInput ) -> nexus.WorkflowHandle[MyOutput]: return await ctx.start_workflow( WorkflowStartedByNexusOperation.run, @@ -43,8 +42,8 @@ async def my_workflow_run_operation( # # Sync operations are free to make arbitrary network calls, or perform CPU-bound # computations. Total execution duration must not exceed 10s. - @sync_operation + @nexusrpc.handler.sync_operation async def my_sync_operation( - self, ctx: StartOperationContext, input: MyInput + self, ctx: nexusrpc.handler.StartOperationContext, input: MyInput ) -> MyOutput: return MyOutput(message=f"Hello {input.name} from sync operation!") From 885920a89a15b2c91c7e34d6109e6641d4c6ee49 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 10 Jul 2025 18:49:31 -0400 Subject: [PATCH 44/46] Skip nexus tests under java test server --- tests/hello_nexus/hello_nexus_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/hello_nexus/hello_nexus_test.py b/tests/hello_nexus/hello_nexus_test.py index c8a5b673..1f304871 100644 --- a/tests/hello_nexus/hello_nexus_test.py +++ b/tests/hello_nexus/hello_nexus_test.py @@ -1,6 +1,8 @@ import asyncio +import pytest from temporalio.client import Client +from temporalio.testing import WorkflowEnvironment import hello_nexus.caller.app import hello_nexus.caller.workflows @@ -8,7 +10,9 @@ from tests.hello_nexus.helpers import create_nexus_endpoint, delete_nexus_endpoint -async def test_nexus_service_basic(client: Client): +async def test_nexus_service_basic(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work under the Java test server") create_response = await create_nexus_endpoint( name=hello_nexus.caller.workflows.NEXUS_ENDPOINT, task_queue=hello_nexus.handler.worker.TASK_QUEUE, From a134d6dce7bceacb0f625d4d9ea7d2b376f17ff7 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 10 Jul 2025 16:35:49 -0400 Subject: [PATCH 45/46] 1.14.1 --- pyproject.toml | 2 +- uv.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6bdfa233..b264bf87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [{ name = "Temporal Technologies Inc", email = "sdk@temporal.io" }] requires-python = "~=3.9" readme = "README.md" license = "MIT" -dependencies = ["temporalio>=1.14.0,<2"] +dependencies = ["temporalio>=1.14.1,<2"] [project.urls] Homepage = "https://github.com/temporalio/samples-python" diff --git a/uv.lock b/uv.lock index c7f17f2b..0096ff3b 100644 --- a/uv.lock +++ b/uv.lock @@ -1189,6 +1189,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/f0/63b06b99b730b9954f8709f6f7d9b8d076fa0a973e472efe278089bde42b/langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15", size = 311812, upload-time = "2024-11-27T17:32:39.569Z" }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + [[package]] name = "marshmallow" version = "3.26.1" @@ -1221,6 +1233,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple/" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "multidict" version = "6.5.0" @@ -1407,6 +1428,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple/" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "numpy" version = "1.26.4" @@ -2081,6 +2111,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple/" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.403" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, +] + [[package]] name = "pytest" version = "7.4.4" @@ -2111,6 +2163,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/4b/7c400506ec484ec999b10133aa8e31af39dfc727042dc6944cd45fd927d0/pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84", size = 14597, upload-time = "2022-03-25T09:43:57.106Z" }, ] +[[package]] +name = "pytest-pretty" +version = "1.3.0" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/d7/c699e0be5401fe9ccad484562f0af9350b4e48c05acf39fb3dab1932128f/pytest_pretty-1.3.0.tar.gz", hash = "sha256:97e9921be40f003e40ae78db078d4a0c1ea42bf73418097b5077970c2cc43bf3", size = 219297, upload-time = "2025-06-04T12:54:37.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/85/2f97a1b65178b0f11c9c77c35417a4cc5b99a80db90dad4734a129844ea5/pytest_pretty-1.3.0-py3-none-any.whl", hash = "sha256:074b9d5783cef9571494543de07e768a4dda92a3e85118d6c7458c67297159b7", size = 5620, upload-time = "2025-06-04T12:54:36.229Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2316,6 +2381,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + [[package]] name = "s3transfer" version = "0.13.0" @@ -2458,7 +2537,7 @@ wheels = [ [[package]] name = "temporalio" -version = "1.14.0" +version = "1.14.1" source = { registry = "https://test.pypi.org/simple/" } dependencies = [ { name = "nexus-rpc" }, @@ -2467,13 +2546,13 @@ dependencies = [ { name = "types-protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://test-files.pythonhosted.org/packages/6d/e9/66ccf5b2d2f45b5943f8431c5a3694baaba7d92402b6bb1a56044ac23aed/temporalio-1.14.0.tar.gz", hash = "sha256:6dbe3b8da0b8c3b7b0079f1985770fd42d826bf98dea312ea2eb46216bcee38b", size = 1605629, upload-time = "2025-07-08T05:47:39.959Z" } +sdist = { url = "https://test-files.pythonhosted.org/packages/40/23/ef5ed581d26112e21c4a6d4ddc2c4eaa5700c0d70b53b07566553e9b7d90/temporalio-1.14.1.tar.gz", hash = "sha256:b240cf56f64add65beb75bd18aa854ac35bdc2505097af5af1e235d611190a9d", size = 1607639, upload-time = "2025-07-10T20:29:47.454Z" } wheels = [ - { url = "https://test-files.pythonhosted.org/packages/c7/49/2246f321a65028cdfec49a480ee4a77bd1c8a6d0efd5ef7eee29d9e17311/temporalio-1.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5a08acc57b9ff0c1c0c2a78bd519448787c6b5fd5695da6f0f6bc2eecd23d7e1", size = 12505134, upload-time = "2025-07-08T05:47:27.949Z" }, - { url = "https://test-files.pythonhosted.org/packages/09/30/9004e8743d3cc76684edfcbed1b0ed1b96ebff624803a13fff9b61e1dc5c/temporalio-1.14.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0dcc5a50e8a9302a808e7e191fd71e0f1ada56003a5b2b03347748966a92d120", size = 12089962, upload-time = "2025-07-08T05:47:30.398Z" }, - { url = "https://test-files.pythonhosted.org/packages/ca/a0/957df6473f210079d5032a65685ddcf26d00cfdbdce4ff9ddcc8b004f7ec/temporalio-1.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7851ff63311af159086741be77071a43a8dc22de6cae4269ca9c907282160cd", size = 12451575, upload-time = "2025-07-08T05:47:33.413Z" }, - { url = "https://test-files.pythonhosted.org/packages/b2/14/682ee77ce0cf624b809ff2fc2bc33666b879ca3461cfbb61d4e94a712044/temporalio-1.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7663373e9d41f63522305fe5c0a246f47d498f9f653b9c19079638440191f611", size = 12687936, upload-time = "2025-07-08T05:47:35.451Z" }, - { url = "https://test-files.pythonhosted.org/packages/43/ac/ecb434b6edb71ae8089a8d87007b7d34293ab48f42487ffde3c15a2f0d30/temporalio-1.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:93a6a2cf5115449b5c9c26a062008cc3c16113facf2268c68cfa3e032c0a02ab", size = 12756882, upload-time = "2025-07-08T05:47:37.707Z" }, + { url = "https://test-files.pythonhosted.org/packages/bd/66/6dc4f5a647a9901cf19e012c442173574babdc879ccaf4cb166662a23ef0/temporalio-1.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ebde00b59af72e512e5837445e4b5b8aa445431d57a71bbeb57a5ba8a93ac8be", size = 12508009, upload-time = "2025-07-10T20:29:34.51Z" }, + { url = "https://test-files.pythonhosted.org/packages/bb/dc/654ebcc92c658180576127ac6dc047fab43b7730f39df4439645e91577fb/temporalio-1.14.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3c21cff8fdc60fbcc9acd91e6c119b0b5f9de7671fe806459f00d68bd4ecae78", size = 12091653, upload-time = "2025-07-10T20:29:37.547Z" }, + { url = "https://test-files.pythonhosted.org/packages/8a/58/7fc3a7bde275c059e42d0279c54e8e66642b67be8eda21b31347f4277186/temporalio-1.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f984b503ae741213fe71128d6193076f3267691561ff3c55dbe798f92e6ee1b", size = 12451995, upload-time = "2025-07-10T20:29:40.186Z" }, + { url = "https://test-files.pythonhosted.org/packages/98/12/14f6a7a1f4aebb7d846469f5c1cd165cce55b793ded6ce5fc315bd83e28f/temporalio-1.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:830cb1a820624a5e64f6c874b5aca6ad9eb841295407dd2011074159a2d28bdb", size = 12688904, upload-time = "2025-07-10T20:29:42.834Z" }, + { url = "https://test-files.pythonhosted.org/packages/b4/ed/c09f1ca41d5ed9f9a777a0ddd5bc225f8300bab8b42bc6751195566706fb/temporalio-1.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:ad4e6a16b42bb34aebec62fb8bbe8f64643d8268ed6d7db337dfe98a76799bb0", size = 12758696, upload-time = "2025-07-10T20:29:45.31Z" }, ] [package.optional-dependencies] @@ -2509,8 +2588,10 @@ dev = [ { name = "frozenlist" }, { name = "isort" }, { name = "mypy" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-pretty" }, { name = "types-pyyaml" }, ] dsl = [ @@ -2534,6 +2615,9 @@ langchain = [ { name = "tqdm" }, { name = "uvicorn", extra = ["standard"] }, ] +nexus = [ + { name = "nexus-rpc" }, +] open-telemetry = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "temporalio", extra = ["opentelemetry"] }, @@ -2554,7 +2638,7 @@ trio-async = [ ] [package.metadata] -requires-dist = [{ name = "temporalio", specifier = ">=1.14.0,<2" }] +requires-dist = [{ name = "temporalio", specifier = ">=1.14.1,<2" }] [package.metadata.requires-dev] bedrock = [{ name = "boto3", specifier = ">=1.34.92,<2" }] @@ -2569,8 +2653,10 @@ dev = [ { name = "frozenlist", specifier = ">=1.4.0,<2" }, { name = "isort", specifier = ">=5.10.1,<6" }, { name = "mypy", specifier = ">=1.4.1,<2" }, + { name = "pyright", specifier = ">=1.1.394" }, { name = "pytest", specifier = ">=7.1.2,<8" }, { name = "pytest-asyncio", specifier = ">=0.18.3,<0.19" }, + { name = "pytest-pretty", specifier = ">=1.3.0" }, { name = "types-pyyaml", specifier = ">=6.0.12.20241230,<7" }, ] dsl = [ @@ -2592,8 +2678,9 @@ langchain = [ { name = "tqdm", specifier = ">=4.62.0,<5" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0.post1,<0.25" }, ] +nexus = [{ name = "nexus-rpc", specifier = ">=1.1.0,<2" }] open-telemetry = [ - { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.18.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "temporalio", extras = ["opentelemetry"] }, ] openai-agents = [ From 0c3fc1dd51816c08c87e5a7751b9c577a841c4f7 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 10 Jul 2025 22:57:32 -0400 Subject: [PATCH 46/46] Skip on 3.9 --- tests/hello_nexus/hello_nexus_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/hello_nexus/hello_nexus_test.py b/tests/hello_nexus/hello_nexus_test.py index 1f304871..09b46252 100644 --- a/tests/hello_nexus/hello_nexus_test.py +++ b/tests/hello_nexus/hello_nexus_test.py @@ -1,4 +1,5 @@ import asyncio +import sys import pytest from temporalio.client import Client @@ -13,6 +14,10 @@ async def test_nexus_service_basic(client: Client, env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip("Nexus tests don't work under the Java test server") + + if sys.version_info[:2] < (3, 10): + pytest.skip("Sample is written for Python >= 3.10") + create_response = await create_nexus_endpoint( name=hello_nexus.caller.workflows.NEXUS_ENDPOINT, task_queue=hello_nexus.handler.worker.TASK_QUEUE,