From 5b55fce7eafeb3d1483b3fd56515227287fc6b57 Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Fri, 17 Apr 2026 17:59:06 -0700 Subject: [PATCH 01/10] Add SANO sample --- nexus_standalone_operations/README.md | 52 ++++++++++++ nexus_standalone_operations/__init__.py | 0 nexus_standalone_operations/handler.py | 42 ++++++++++ nexus_standalone_operations/service.py | 39 +++++++++ nexus_standalone_operations/starter.py | 74 +++++++++++++++++ nexus_standalone_operations/worker.py | 41 ++++++++++ pyproject.toml | 3 + tests/nexus_standalone_operations/__init__.py | 0 .../nexus_standalone_operations_test.py | 81 +++++++++++++++++++ uv.lock | 26 +++++- 10 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 nexus_standalone_operations/README.md create mode 100644 nexus_standalone_operations/__init__.py create mode 100644 nexus_standalone_operations/handler.py create mode 100644 nexus_standalone_operations/service.py create mode 100644 nexus_standalone_operations/starter.py create mode 100644 nexus_standalone_operations/worker.py create mode 100644 tests/nexus_standalone_operations/__init__.py create mode 100644 tests/nexus_standalone_operations/nexus_standalone_operations_test.py diff --git a/nexus_standalone_operations/README.md b/nexus_standalone_operations/README.md new file mode 100644 index 00000000..0a776636 --- /dev/null +++ b/nexus_standalone_operations/README.md @@ -0,0 +1,52 @@ +This sample demonstrates how to execute Nexus operations directly from client code, +without wrapping them in a workflow. It shows both synchronous and asynchronous +(workflow-backed) operations, plus listing and counting operations. + +## Note: Standalone Nexus operations require a server version that supports this feature. + +### Sample directory structure + +- [service.py](./service.py) - Nexus service definition with echo (sync) and hello (async) operations +- [handler.py](./handler.py) - Nexus operation handlers and the backing workflow for the async operation +- [worker.py](./worker.py) - Temporal worker that hosts the Nexus service +- [starter.py](./starter.py) - Client that executes standalone Nexus operations + + +### Instructions + +Start a Temporal server. (See the main samples repo [README](../README.md)). + +Create the Nexus endpoint: + +``` +temporal operator nexus endpoint create \ + --name nexus-standalone-operations-endpoint \ + --target-namespace default \ + --target-task-queue nexus-standalone-operations +``` + +In one terminal, start the worker: +``` +uv run nexus_standalone_operations/worker.py +``` + +In another terminal, run the starter: +``` +uv run nexus_standalone_operations/starter.py +``` + +### Expected output + +``` +Echo result: hello +Hello result: Hello, World! + +Listing Nexus operations: + OperationId: echo-..., Operation: echo, Status: COMPLETED + OperationId: hello-..., Operation: hello, Status: COMPLETED + +Total Nexus operations: 2 +``` + +If you run the starter code multiple times, you should see additional operations in the listing results, as more operations are run. +The same goes for the total number of operations. \ No newline at end of file diff --git a/nexus_standalone_operations/__init__.py b/nexus_standalone_operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nexus_standalone_operations/handler.py b/nexus_standalone_operations/handler.py new file mode 100644 index 00000000..af867058 --- /dev/null +++ b/nexus_standalone_operations/handler.py @@ -0,0 +1,42 @@ +"""Nexus service handler and backing workflow for standalone operations sample.""" + +from __future__ import annotations + +import uuid + +import nexusrpc.handler +from temporalio import nexus, workflow + +from nexus_standalone_operations.service import ( + EchoInput, + EchoOutput, + HelloInput, + HelloOutput, + MyNexusService, +) + + +@workflow.defn +class HelloWorkflow: + @workflow.run + async def run(self, input: HelloInput) -> HelloOutput: + return HelloOutput(greeting=f"Hello, {input.name}!") + + +@nexusrpc.handler.service_handler(service=MyNexusService) +class MyNexusServiceHandler: + @nexusrpc.handler.sync_operation + async def echo( + self, _ctx: nexusrpc.handler.StartOperationContext, input: EchoInput + ) -> EchoOutput: + return EchoOutput(message=input.message) + + @nexus.workflow_run_operation + async def hello( + self, ctx: nexus.WorkflowRunOperationContext, input: HelloInput + ) -> nexus.WorkflowHandle[HelloOutput]: + return await ctx.start_workflow( + HelloWorkflow.run, + input, + id=str(uuid.uuid4()), + ) diff --git a/nexus_standalone_operations/service.py b/nexus_standalone_operations/service.py new file mode 100644 index 00000000..a8a906db --- /dev/null +++ b/nexus_standalone_operations/service.py @@ -0,0 +1,39 @@ +"""Nexus service definition for standalone operations sample. + +Defines a Nexus service with two operations: +- echo: a synchronous operation that echoes the input message +- hello: an asynchronous (workflow-backed) operation that returns a greeting + +This service definition is used by both the handler (to validate operation +signatures) and the client (to create type-safe nexus clients). +""" + +from dataclasses import dataclass + +import nexusrpc + + +@dataclass +class EchoInput: + message: str + + +@dataclass +class EchoOutput: + message: str + + +@dataclass +class HelloInput: + name: str + + +@dataclass +class HelloOutput: + greeting: str + + +@nexusrpc.service +class MyNexusService: + echo: nexusrpc.Operation[EchoInput, EchoOutput] + hello: nexusrpc.Operation[HelloInput, HelloOutput] diff --git a/nexus_standalone_operations/starter.py b/nexus_standalone_operations/starter.py new file mode 100644 index 00000000..527cfdd3 --- /dev/null +++ b/nexus_standalone_operations/starter.py @@ -0,0 +1,74 @@ +"""Starter that demonstrates standalone Nexus operation execution. + +Unlike other Nexus samples that call operations from within a workflow, this +sample executes Nexus operations directly from client code using the standalone +Nexus operation APIs. +""" + +import asyncio +import uuid +from datetime import timedelta + +from temporalio.client import Client +from temporalio.envconfig import ClientConfig + +from nexus_standalone_operations.service import ( + EchoInput, + HelloInput, + MyNexusService, +) + +ENDPOINT_NAME = "nexus-standalone-operations-endpoint" + + +async def main() -> None: + config = ClientConfig.load_client_connect_config() + _ = config.setdefault("target_host", "localhost:7233") + client = await Client.connect(**config) + + # Create a typed NexusClient bound to the endpoint and service. + # The endpoint must be pre-created on the server (see README). + nexus_client = client.create_nexus_client( + service=MyNexusService, endpoint=ENDPOINT_NAME + ) + + # Start sync echo operation and await the result immediately. + echo_result = await nexus_client.execute_operation( + MyNexusService.echo, + EchoInput(message="hello"), + id=f"echo-{uuid.uuid4()}", + schedule_to_close_timeout=timedelta(seconds=10), + ) + print(f"Echo result: {echo_result.message}") + + # Start async (workflow-backed) hello operation and get a NexusOperationHandle. + handle = await nexus_client.start_operation( + MyNexusService.hello, + HelloInput(name="World"), + id=f"hello-{uuid.uuid4()}", + schedule_to_close_timeout=timedelta(seconds=10), + ) + + print(f"\nStarted `MyNexusService.Hello`. OperationID: {handle.operation_id}") + + # Use the NexusOperationHandle to await the result of the operation. + hello_result = await handle.result() + print(f"`MyNexusService.Hello` result: {hello_result.greeting}") + + # List nexus operations. + query = f'Endpoint = "{ENDPOINT_NAME}"' + print("\nListing Nexus operations:") + async for op in client.list_nexus_operations(query): + print( + f" OperationId: {op.operation_id},", + f" Operation: {op.operation},", + f" Status: {op.status.name}", + ) + + # Count nexus operations. + count = await client.count_nexus_operations(query) + print(f"\nTotal Nexus operations: {count.count}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/nexus_standalone_operations/worker.py b/nexus_standalone_operations/worker.py new file mode 100644 index 00000000..0de4ac3b --- /dev/null +++ b/nexus_standalone_operations/worker.py @@ -0,0 +1,41 @@ +"""Worker that hosts the Nexus service for standalone operations sample.""" + +import asyncio +import logging + +from temporalio.client import Client +from temporalio.envconfig import ClientConfig +from temporalio.worker import Worker + +from nexus_standalone_operations.handler import HelloWorkflow, MyNexusServiceHandler + +interrupt_event = asyncio.Event() + +TASK_QUEUE = "nexus-standalone-operations" + + +async def main() -> None: + logging.basicConfig(level=logging.INFO) + + config = ClientConfig.load_client_connect_config() + _ = config.setdefault("target_host", "localhost:7233") + client = await Client.connect(**config) + + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[HelloWorkflow], + nexus_service_handlers=[MyNexusServiceHandler()], + ): + 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/pyproject.toml b/pyproject.toml index 9be7bbca..249e1d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,3 +164,6 @@ ignore_errors = true [[tool.mypy.overrides]] module = "opentelemetry.*" ignore_errors = true + +[tool.uv.sources] +temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "amazzeo/sano" } diff --git a/tests/nexus_standalone_operations/__init__.py b/tests/nexus_standalone_operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/nexus_standalone_operations/nexus_standalone_operations_test.py b/tests/nexus_standalone_operations/nexus_standalone_operations_test.py new file mode 100644 index 00000000..a8302b93 --- /dev/null +++ b/tests/nexus_standalone_operations/nexus_standalone_operations_test.py @@ -0,0 +1,81 @@ +import asyncio +import os +import uuid +from datetime import timedelta + +import pytest +from temporalio.client import Client, NexusOperationFailureError +from temporalio.service import RPCError +from temporalio.worker import Worker + +from nexus_standalone_operations.handler import HelloWorkflow, MyNexusServiceHandler +from nexus_standalone_operations.service import ( + EchoInput, + EchoOutput, + HelloInput, + HelloOutput, + MyNexusService, +) +from nexus_standalone_operations.worker import TASK_QUEUE +from tests.helpers.nexus import create_nexus_endpoint, delete_nexus_endpoint + + +async def test_nexus_standalone_operations(client: Client): + if not os.getenv("ENABLE_STANDALONE_NEXUS_TESTS"): + pytest.skip( + "Standalone Nexus operations not yet supported by default dev server. Set ENABLE_STANDALONE_NEXUS_TESTS=1 to enable." + ) + + endpoint_name = f"test-nexus-standalone-{uuid.uuid4()}" + + create_response = await create_nexus_endpoint( + name=endpoint_name, + task_queue=TASK_QUEUE, + client=client, + ) + try: + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[HelloWorkflow], + nexus_service_handlers=[MyNexusServiceHandler()], + ): + nexus_client = client.create_nexus_client( + service=MyNexusService, endpoint=endpoint_name + ) + + # Test sync echo operation (with retry for endpoint propagation) + echo_result = None + for _ in range(30): + try: + echo_result = await nexus_client.execute_operation( + MyNexusService.echo, + EchoInput(message="test-echo"), + id=str(uuid.uuid4()), + schedule_to_close_timeout=timedelta(seconds=10), + ) + break + except (RPCError, NexusOperationFailureError): + await asyncio.sleep(0.5) + assert isinstance(echo_result, EchoOutput) + assert echo_result.message == "test-echo" + + # Test async hello operation + hello_result = await nexus_client.execute_operation( + MyNexusService.hello, + HelloInput(name="Test"), + id=str(uuid.uuid4()), + schedule_to_close_timeout=timedelta(seconds=10), + ) + assert isinstance(hello_result, HelloOutput) + assert hello_result.greeting == "Hello, Test!" + + # Test count operations + count = await client.count_nexus_operations(f'Endpoint = "{endpoint_name}"') + assert count.count >= 0 + finally: + _ = await delete_nexus_endpoint( + id=create_response.endpoint.id, + version=create_response.endpoint.version, + client=client, + ) diff --git a/uv.lock b/uv.lock index 5ff44103..84998a0c 100644 --- a/uv.lock +++ b/uv.lock @@ -3812,7 +3812,13 @@ trio-async = [ [package.metadata] requires-dist = [ { name = "protobuf", specifier = ">=5.29.6,<6" }, +<<<<<<< HEAD { name = "temporalio", specifier = ">=1.28.0,<2" }, +||||||| parent of 3c655c0 (Add SANO sample) + { name = "temporalio", specifier = ">=1.27.2,<2" }, +======= + { name = "temporalio", git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, +>>>>>>> 3c655c0 (Add SANO sample) ] [package.metadata.requires-dev] @@ -3856,22 +3862,40 @@ langgraph = [ { name = "langchain", specifier = ">=0.3.0" }, { name = "langchain-anthropic", specifier = ">=0.3.0" }, { name = "langgraph", specifier = ">=1.1.3" }, +<<<<<<< HEAD { name = "temporalio", extras = ["langgraph", "langsmith"], specifier = ">=1.28.0" }, +||||||| parent of 3c655c0 (Add SANO sample) + { name = "temporalio", extras = ["langgraph", "langsmith"], specifier = ">=1.27.0" }, +======= + { name = "temporalio", extras = ["langgraph", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, +>>>>>>> 3c655c0 (Add SANO sample) ] langsmith-tracing = [ { name = "langsmith", specifier = ">=0.7.0" }, { name = "openai", specifier = ">=1.4.0" }, +<<<<<<< HEAD { name = "temporalio", extras = ["pydantic", "langsmith"], specifier = ">=1.28.0" }, +||||||| parent of 3c655c0 (Add SANO sample) + { name = "temporalio", extras = ["pydantic", "langsmith"], specifier = ">=1.27.0" }, +======= + { name = "temporalio", extras = ["pydantic", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, +>>>>>>> 3c655c0 (Add SANO sample) ] nexus = [{ name = "nexus-rpc", specifier = ">=1.1.0,<2" }] open-telemetry = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "temporalio", extras = ["opentelemetry"] }, + { name = "temporalio", extras = ["opentelemetry"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ] openai-agents = [ { name = "openai-agents", extras = ["litellm"], specifier = ">=0.14.1" }, { name = "requests", specifier = ">=2.32.0,<3" }, +<<<<<<< HEAD { name = "temporalio", extras = ["openai-agents", "opentelemetry"], specifier = ">=1.28.0" }, +||||||| parent of 3c655c0 (Add SANO sample) + { name = "temporalio", extras = ["openai-agents", "opentelemetry"], specifier = ">=1.27.0" }, +======= + { name = "temporalio", extras = ["openai-agents", "opentelemetry"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, +>>>>>>> 3c655c0 (Add SANO sample) ] pydantic-converter = [{ name = "pydantic", specifier = ">=2.10.6,<3" }] sentry = [{ name = "sentry-sdk", specifier = ">=2.13.0" }] From 20ad2cc6cd4431f397c9de9bb1f402f0afa8c5b6 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Thu, 21 May 2026 13:42:06 -0500 Subject: [PATCH 02/10] Redis external storage driver (#287) * Redis external storage driver * Fix lints * Fix CI tests * Fix yarl error in CI * Revert accidental README.md edit, and add external_storage_redis. * Move redis test workflow to tests/ * Update test_redis_worker.py * Constrain langsmith to allow CI to pass --- uv.lock | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/uv.lock b/uv.lock index 84998a0c..087b166d 100644 --- a/uv.lock +++ b/uv.lock @@ -3862,24 +3862,12 @@ langgraph = [ { name = "langchain", specifier = ">=0.3.0" }, { name = "langchain-anthropic", specifier = ">=0.3.0" }, { name = "langgraph", specifier = ">=1.1.3" }, -<<<<<<< HEAD { name = "temporalio", extras = ["langgraph", "langsmith"], specifier = ">=1.28.0" }, -||||||| parent of 3c655c0 (Add SANO sample) - { name = "temporalio", extras = ["langgraph", "langsmith"], specifier = ">=1.27.0" }, -======= - { name = "temporalio", extras = ["langgraph", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ->>>>>>> 3c655c0 (Add SANO sample) ] langsmith-tracing = [ { name = "langsmith", specifier = ">=0.7.0" }, { name = "openai", specifier = ">=1.4.0" }, -<<<<<<< HEAD { name = "temporalio", extras = ["pydantic", "langsmith"], specifier = ">=1.28.0" }, -||||||| parent of 3c655c0 (Add SANO sample) - { name = "temporalio", extras = ["pydantic", "langsmith"], specifier = ">=1.27.0" }, -======= - { name = "temporalio", extras = ["pydantic", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ->>>>>>> 3c655c0 (Add SANO sample) ] nexus = [{ name = "nexus-rpc", specifier = ">=1.1.0,<2" }] open-telemetry = [ @@ -3889,13 +3877,7 @@ open-telemetry = [ openai-agents = [ { name = "openai-agents", extras = ["litellm"], specifier = ">=0.14.1" }, { name = "requests", specifier = ">=2.32.0,<3" }, -<<<<<<< HEAD { name = "temporalio", extras = ["openai-agents", "opentelemetry"], specifier = ">=1.28.0" }, -||||||| parent of 3c655c0 (Add SANO sample) - { name = "temporalio", extras = ["openai-agents", "opentelemetry"], specifier = ">=1.27.0" }, -======= - { name = "temporalio", extras = ["openai-agents", "opentelemetry"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ->>>>>>> 3c655c0 (Add SANO sample) ] pydantic-converter = [{ name = "pydantic", specifier = ">=2.10.6,<3" }] sentry = [{ name = "sentry-sdk", specifier = ">=2.13.0" }] From a8b65d3a1c4591b8921c0f151b226dbee6db1a24 Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Wed, 3 Jun 2026 16:31:40 -0700 Subject: [PATCH 03/10] Update to main of python sdk --- nexus_standalone_operations/starter.py | 2 +- pyproject.toml | 2 +- uv.lock | 18 ++++++------------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/nexus_standalone_operations/starter.py b/nexus_standalone_operations/starter.py index 527cfdd3..54d5d965 100644 --- a/nexus_standalone_operations/starter.py +++ b/nexus_standalone_operations/starter.py @@ -60,7 +60,7 @@ async def main() -> None: print("\nListing Nexus operations:") async for op in client.list_nexus_operations(query): print( - f" OperationId: {op.operation_id},", + f" OperationId: {op.operation_id},", f" Operation: {op.operation},", f" Status: {op.status.name}", ) diff --git a/pyproject.toml b/pyproject.toml index 249e1d1c..f32383e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,4 +166,4 @@ module = "opentelemetry.*" ignore_errors = true [tool.uv.sources] -temporalio = { git = "https://github.com/temporalio/sdk-python", branch = "amazzeo/sano" } +temporalio = { git = "https://github.com/temporalio/sdk-python" } diff --git a/uv.lock b/uv.lock index 087b166d..377d8912 100644 --- a/uv.lock +++ b/uv.lock @@ -3686,8 +3686,8 @@ wheels = [ [[package]] name = "temporalio" -version = "1.28.0" -source = { registry = "https://pypi.org/simple" } +version = "1.27.2" +source = { git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano#b2bd5ea992ef8975ade1d224d8a806d8ee8b9b3a" } dependencies = [ { name = "nexus-rpc" }, { name = "protobuf" }, @@ -3812,13 +3812,7 @@ trio-async = [ [package.metadata] requires-dist = [ { name = "protobuf", specifier = ">=5.29.6,<6" }, -<<<<<<< HEAD - { name = "temporalio", specifier = ">=1.28.0,<2" }, -||||||| parent of 3c655c0 (Add SANO sample) - { name = "temporalio", specifier = ">=1.27.2,<2" }, -======= { name = "temporalio", git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ->>>>>>> 3c655c0 (Add SANO sample) ] [package.metadata.requires-dev] @@ -3862,22 +3856,22 @@ langgraph = [ { name = "langchain", specifier = ">=0.3.0" }, { name = "langchain-anthropic", specifier = ">=0.3.0" }, { name = "langgraph", specifier = ">=1.1.3" }, - { name = "temporalio", extras = ["langgraph", "langsmith"], specifier = ">=1.28.0" }, + { name = "temporalio", extras = ["langgraph", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ] langsmith-tracing = [ { name = "langsmith", specifier = ">=0.7.0" }, { name = "openai", specifier = ">=1.4.0" }, - { name = "temporalio", extras = ["pydantic", "langsmith"], specifier = ">=1.28.0" }, + { name = "temporalio", extras = ["pydantic", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ] nexus = [{ name = "nexus-rpc", specifier = ">=1.1.0,<2" }] open-telemetry = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "temporalio", extras = ["opentelemetry"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, + { name = "temporalio", extras = ["opentelemetry"], git = "https://github.com/temporalio/sdk-python" }, ] openai-agents = [ { name = "openai-agents", extras = ["litellm"], specifier = ">=0.14.1" }, { name = "requests", specifier = ">=2.32.0,<3" }, - { name = "temporalio", extras = ["openai-agents", "opentelemetry"], specifier = ">=1.28.0" }, + { name = "temporalio", extras = ["openai-agents", "opentelemetry"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, ] pydantic-converter = [{ name = "pydantic", specifier = ">=2.10.6,<3" }] sentry = [{ name = "sentry-sdk", specifier = ">=2.13.0" }] From 98f1559d31c2a0831baa51ac25e1b951703d4fe6 Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Thu, 4 Jun 2026 14:05:22 -0700 Subject: [PATCH 04/10] Update to python sdk 1.28.0 --- nexus_standalone_operations/starter.py | 2 +- pyproject.toml | 3 --- uv.lock | 14 +++++++------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/nexus_standalone_operations/starter.py b/nexus_standalone_operations/starter.py index 54d5d965..11d52e53 100644 --- a/nexus_standalone_operations/starter.py +++ b/nexus_standalone_operations/starter.py @@ -56,8 +56,8 @@ async def main() -> None: print(f"`MyNexusService.Hello` result: {hello_result.greeting}") # List nexus operations. - query = f'Endpoint = "{ENDPOINT_NAME}"' print("\nListing Nexus operations:") + query = f'Endpoint = "{ENDPOINT_NAME}"' async for op in client.list_nexus_operations(query): print( f" OperationId: {op.operation_id},", diff --git a/pyproject.toml b/pyproject.toml index f32383e1..9be7bbca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,6 +164,3 @@ ignore_errors = true [[tool.mypy.overrides]] module = "opentelemetry.*" ignore_errors = true - -[tool.uv.sources] -temporalio = { git = "https://github.com/temporalio/sdk-python" } diff --git a/uv.lock b/uv.lock index 377d8912..5ff44103 100644 --- a/uv.lock +++ b/uv.lock @@ -3686,8 +3686,8 @@ wheels = [ [[package]] name = "temporalio" -version = "1.27.2" -source = { git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano#b2bd5ea992ef8975ade1d224d8a806d8ee8b9b3a" } +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nexus-rpc" }, { name = "protobuf" }, @@ -3812,7 +3812,7 @@ trio-async = [ [package.metadata] requires-dist = [ { name = "protobuf", specifier = ">=5.29.6,<6" }, - { name = "temporalio", git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, + { name = "temporalio", specifier = ">=1.28.0,<2" }, ] [package.metadata.requires-dev] @@ -3856,22 +3856,22 @@ langgraph = [ { name = "langchain", specifier = ">=0.3.0" }, { name = "langchain-anthropic", specifier = ">=0.3.0" }, { name = "langgraph", specifier = ">=1.1.3" }, - { name = "temporalio", extras = ["langgraph", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, + { name = "temporalio", extras = ["langgraph", "langsmith"], specifier = ">=1.28.0" }, ] langsmith-tracing = [ { name = "langsmith", specifier = ">=0.7.0" }, { name = "openai", specifier = ">=1.4.0" }, - { name = "temporalio", extras = ["pydantic", "langsmith"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, + { name = "temporalio", extras = ["pydantic", "langsmith"], specifier = ">=1.28.0" }, ] nexus = [{ name = "nexus-rpc", specifier = ">=1.1.0,<2" }] open-telemetry = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "temporalio", extras = ["opentelemetry"], git = "https://github.com/temporalio/sdk-python" }, + { name = "temporalio", extras = ["opentelemetry"] }, ] openai-agents = [ { name = "openai-agents", extras = ["litellm"], specifier = ">=0.14.1" }, { name = "requests", specifier = ">=2.32.0,<3" }, - { name = "temporalio", extras = ["openai-agents", "opentelemetry"], git = "https://github.com/temporalio/sdk-python?branch=amazzeo%2Fsano" }, + { name = "temporalio", extras = ["openai-agents", "opentelemetry"], specifier = ">=1.28.0" }, ] pydantic-converter = [{ name = "pydantic", specifier = ">=2.10.6,<3" }] sentry = [{ name = "sentry-sdk", specifier = ">=2.13.0" }] From b3f78e3b81bf40c34efd332f9d500659602a0bae Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Thu, 4 Jun 2026 14:11:22 -0700 Subject: [PATCH 05/10] Fix test. Use sano dev server and add required dynamic config --- tests/conftest.py | 7 ++++- .../nexus_standalone_operations_test.py | 30 +++++-------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 65de246e..ba39a3e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,12 +42,17 @@ async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]: env_type = request.config.getoption("--workflow-environment") if env_type == "local": env = await WorkflowEnvironment.start_local( + dev_server_download_version="v1.7.1-standalone-nexus-operations", dev_server_extra_args=[ "--dynamic-config-value", "frontend.enableExecuteMultiOperation=true", "--dynamic-config-value", "system.enableEagerWorkflowStart=true", - ] + "--dynamic-config-value", + "nexusoperation.enableStandalone=true", + "--dynamic-config-value", + "history.enableChasmCallbacks=true", + ], ) elif env_type == "time-skipping": env = await WorkflowEnvironment.start_time_skipping() diff --git a/tests/nexus_standalone_operations/nexus_standalone_operations_test.py b/tests/nexus_standalone_operations/nexus_standalone_operations_test.py index a8302b93..c129b9f7 100644 --- a/tests/nexus_standalone_operations/nexus_standalone_operations_test.py +++ b/tests/nexus_standalone_operations/nexus_standalone_operations_test.py @@ -1,11 +1,7 @@ -import asyncio -import os import uuid from datetime import timedelta -import pytest -from temporalio.client import Client, NexusOperationFailureError -from temporalio.service import RPCError +from temporalio.client import Client from temporalio.worker import Worker from nexus_standalone_operations.handler import HelloWorkflow, MyNexusServiceHandler @@ -21,11 +17,6 @@ async def test_nexus_standalone_operations(client: Client): - if not os.getenv("ENABLE_STANDALONE_NEXUS_TESTS"): - pytest.skip( - "Standalone Nexus operations not yet supported by default dev server. Set ENABLE_STANDALONE_NEXUS_TESTS=1 to enable." - ) - endpoint_name = f"test-nexus-standalone-{uuid.uuid4()}" create_response = await create_nexus_endpoint( @@ -44,19 +35,14 @@ async def test_nexus_standalone_operations(client: Client): service=MyNexusService, endpoint=endpoint_name ) - # Test sync echo operation (with retry for endpoint propagation) + # Test sync echo operation echo_result = None - for _ in range(30): - try: - echo_result = await nexus_client.execute_operation( - MyNexusService.echo, - EchoInput(message="test-echo"), - id=str(uuid.uuid4()), - schedule_to_close_timeout=timedelta(seconds=10), - ) - break - except (RPCError, NexusOperationFailureError): - await asyncio.sleep(0.5) + echo_result = await nexus_client.execute_operation( + MyNexusService.echo, + EchoInput(message="test-echo"), + id=str(uuid.uuid4()), + schedule_to_close_timeout=timedelta(seconds=10), + ) assert isinstance(echo_result, EchoOutput) assert echo_result.message == "test-echo" From d8e50cb424c8c91725e65cbcf15e0c9dc8a366fc Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Thu, 4 Jun 2026 14:27:07 -0700 Subject: [PATCH 06/10] Skip test on timeskipping server --- .../nexus_standalone_operations_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/nexus_standalone_operations/nexus_standalone_operations_test.py b/tests/nexus_standalone_operations/nexus_standalone_operations_test.py index c129b9f7..b0bd2dad 100644 --- a/tests/nexus_standalone_operations/nexus_standalone_operations_test.py +++ b/tests/nexus_standalone_operations/nexus_standalone_operations_test.py @@ -1,7 +1,9 @@ import uuid from datetime import timedelta +import pytest from temporalio.client import Client +from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker from nexus_standalone_operations.handler import HelloWorkflow, MyNexusServiceHandler @@ -16,7 +18,10 @@ from tests.helpers.nexus import create_nexus_endpoint, delete_nexus_endpoint -async def test_nexus_standalone_operations(client: Client): +async def test_nexus_standalone_operations(client: Client, env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip("Time Skipping server does not support standalone nexus operations") + endpoint_name = f"test-nexus-standalone-{uuid.uuid4()}" create_response = await create_nexus_endpoint( From 683ef534d93e1dc37d3154b4ed8d9dbf76457eea Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Fri, 5 Jun 2026 09:52:44 -0700 Subject: [PATCH 07/10] update readme w/ dynamic config values --- nexus_standalone_operations/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nexus_standalone_operations/README.md b/nexus_standalone_operations/README.md index 0a776636..e1100f33 100644 --- a/nexus_standalone_operations/README.md +++ b/nexus_standalone_operations/README.md @@ -14,7 +14,13 @@ without wrapping them in a workflow. It shows both synchronous and asynchronous ### Instructions -Start a Temporal server. (See the main samples repo [README](../README.md)). +Start a Temporal dev server with the dynamic config flags required for standalone Nexus operations: + +```bash +temporal server start-dev \ + --dynamic-config-value "nexusoperation.enableStandalone=true" \ + --dynamic-config-value "history.enableChasmCallbacks=true" +``` Create the Nexus endpoint: From a810f9a910b44f89a7a492c09499dde4aeecf234 Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Fri, 5 Jun 2026 10:34:00 -0700 Subject: [PATCH 08/10] Update readme with link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 801fd2ed..d23453c5 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ Some examples require extra dependencies. See each sample's directory for specif * [Nexus Messaging](nexus_messaging): Demonstrates how send signal, update and query messages through Nexus. This contains two samples, one sending messages to an existing workflow and a second that creates a workflow through Nexus and sends messages to it. +* [nexus_standalone_operations](nexus_standalone_operations) - Execute Nexus operations directly from client code, +without wrapping them in a workflow. * [open_telemetry](open_telemetry) - Trace workflows with OpenTelemetry. * [patching](patching) - Alter workflows safely with `patch` and `deprecate_patch`. * [polling](polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion. From 23d9e9242eb4ca3fce1c65d4ce34d845beef984d Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Fri, 5 Jun 2026 10:42:49 -0700 Subject: [PATCH 09/10] use new temporal operation instead of workflow run operation --- nexus_standalone_operations/handler.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nexus_standalone_operations/handler.py b/nexus_standalone_operations/handler.py index af867058..da18e947 100644 --- a/nexus_standalone_operations/handler.py +++ b/nexus_standalone_operations/handler.py @@ -31,11 +31,14 @@ async def echo( ) -> EchoOutput: return EchoOutput(message=input.message) - @nexus.workflow_run_operation + @nexus.temporal_operation async def hello( - self, ctx: nexus.WorkflowRunOperationContext, input: HelloInput - ) -> nexus.WorkflowHandle[HelloOutput]: - return await ctx.start_workflow( + self, + _ctx: nexus.TemporalStartOperationContext, + client: nexus.TemporalNexusClient, + input: HelloInput, + ) -> nexus.TemporalOperationResult[HelloOutput]: + return await client.start_workflow( HelloWorkflow.run, input, id=str(uuid.uuid4()), From 429a066f95c5441471c90727deab27bfbddfdc7b Mon Sep 17 00:00:00 2001 From: Alex Mazzeo Date: Fri, 5 Jun 2026 11:30:14 -0700 Subject: [PATCH 10/10] Bump to v1.7.2-standalone-nexus-operations cli version. Add callouts about release phase. Add dev server links. --- nexus_standalone_operations/README.md | 10 +++++++++- tests/conftest.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nexus_standalone_operations/README.md b/nexus_standalone_operations/README.md index e1100f33..3307463a 100644 --- a/nexus_standalone_operations/README.md +++ b/nexus_standalone_operations/README.md @@ -2,7 +2,12 @@ This sample demonstrates how to execute Nexus operations directly from client co without wrapping them in a workflow. It shows both synchronous and asynchronous (workflow-backed) operations, plus listing and counting operations. -## Note: Standalone Nexus operations require a server version that supports this feature. + +### Temporal Python SDK support for Standalone Nexus Operations is at [Pre-release](https://docs.temporal.io/evaluate/development-production-features/release-stages#pre-release). + +All APIs are experimental and may be subject to backwards-incompatible changes. + +Standalone Nexus operations require a server version that supports this feature. Use the dev server build at https://github.com/temporalio/cli/releases/tag/v1.7.2-standalone-nexus-operations. ### Sample directory structure @@ -14,6 +19,9 @@ without wrapping them in a workflow. It shows both synchronous and asynchronous ### Instructions +Run the [Temporal dev server build that supports standalone Nexus operations](https://github.com/temporalio/cli/releases/tag/v1.7.2-standalone-nexus-operations). +(If you are going to run locally, you will want to start it in another terminal; this command is blocking and runs until it receives a SIGINT (Ctrl + C) command.) + Start a Temporal dev server with the dynamic config flags required for standalone Nexus operations: ```bash diff --git a/tests/conftest.py b/tests/conftest.py index ba39a3e0..3d054e71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]: env_type = request.config.getoption("--workflow-environment") if env_type == "local": env = await WorkflowEnvironment.start_local( - dev_server_download_version="v1.7.1-standalone-nexus-operations", + dev_server_download_version="v1.7.2-standalone-nexus-operations", dev_server_extra_args=[ "--dynamic-config-value", "frontend.enableExecuteMultiOperation=true",