diff --git a/README.md b/README.md index 587a3098c..31102c727 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Some examples require extra dependencies. See each sample's directory for specif * [hello_search_attributes](hello/hello_search_attributes.py) - Start workflow with search attributes then change while running. * [hello_signal](hello/hello_signal.py) - Send signals to a workflow. + * [hello_sync_client](hello/hello_sync_client.py) - Call the async Temporal client from synchronous Python code. * [hello update](hello/hello_update.py) - Send a request to and a response from a client to a workflow execution. * [activity_worker](activity_worker) - Use Python activities from a workflow in another language. diff --git a/hello/README.md b/hello/README.md index 7b5ca6ebb..7d6c0c364 100644 --- a/hello/README.md +++ b/hello/README.md @@ -44,8 +44,22 @@ Replace `hello/hello_activity.py` in the command with any other example filename * [hello_search_attributes](hello_search_attributes.py) - Start workflow with search attributes then change while running. * [hello_signal](hello_signal.py) - Send signals to a workflow. +* [hello_sync_client](hello_sync_client.py) - Call the async Temporal client from synchronous Python code. * [hello_update](hello_update.py) - **Send a request to and a response from a client to a workflow execution.** Note: To enable the workflow update, set the `frontend.enableUpdateWorkflowExecution` dynamic config value to true. temporal server start-dev --dynamic-config-value frontend.enableUpdateWorkflowExecution=true + +## Calling the client from synchronous code + +Run `uv run hello/hello_sync_client.py` to call `greet("World")` from a normal +synchronous function and print `Result: Hello, World!`. The function uses +`asyncio.run()` to connect a client, execute a workflow, and wait for its result. +The sample also starts and stops a worker so it can run on its own. + +Use this pattern from a thread that has no running event loop. If your application +is already asynchronous, await the SDK methods directly. Each call creates a new +event loop, so create and use the client inside that call rather than reusing a +client from an earlier call. For applications making many calls, prefer a +long-lived async client and a separately running worker. diff --git a/hello/hello_sync_client.py b/hello/hello_sync_client.py new file mode 100644 index 000000000..1c1a3a58b --- /dev/null +++ b/hello/hello_sync_client.py @@ -0,0 +1,50 @@ +import asyncio +import uuid + +from temporalio import workflow +from temporalio.client import Client +from temporalio.envconfig import ClientConfig +from temporalio.worker import Worker + + +@workflow.defn +class GreetingWorkflow: + @workflow.run + async def run(self, name: str) -> str: + return f"Hello, {name}!" + + +async def _execute_greeting(name: str) -> str: + # Create and use the client on the same event loop. Do not cache this client + # and reuse it across calls to asyncio.run(), which creates a new loop. + config = ClientConfig.load_client_connect_config() + config.setdefault("target_host", "localhost:7233") + client = await Client.connect(**config) + task_queue = f"hello-sync-client-{uuid.uuid4()}" + + # Keep the sample self-contained by running a worker here. In a production + # application, workers usually run in a separate process on a shared queue. + async with Worker(client, task_queue=task_queue, workflows=[GreetingWorkflow]): + return await client.execute_workflow( + GreetingWorkflow.run, + name, + id=f"hello-sync-client-{uuid.uuid4()}", + task_queue=task_queue, + ) + + +def greet(name: str) -> str: + """Execute a workflow from synchronous code and wait for its result.""" + # This is the boundary between the synchronous application and the async SDK. + # Call it from a thread without a running event loop. In an async application, + # await the async SDK directly instead. + return asyncio.run(_execute_greeting(name)) + + +def main() -> None: + # Existing synchronous application code does not need async/await. + print(f"Result: {greet('World')}") + + +if __name__ == "__main__": + main() diff --git a/tests/hello/hello_sync_client_test.py b/tests/hello/hello_sync_client_test.py new file mode 100644 index 000000000..8c88670bc --- /dev/null +++ b/tests/hello/hello_sync_client_test.py @@ -0,0 +1,29 @@ +import asyncio + +import pytest +from temporalio.client import Client +from temporalio.envconfig import ClientConfig + +from hello.hello_sync_client import greet + + +async def test_greet_from_sync_code(client: Client, monkeypatch: pytest.MonkeyPatch): + # Connect the sample's own client to the test environment (local or + # time-skipping), rather than sharing a client across event loops. + monkeypatch.setattr( + ClientConfig, + "load_client_connect_config", + lambda: { + "target_host": client.service_client.config.target_host, + "namespace": client.namespace, + }, + ) + + def call_from_sync_code(): + # Repeated calls create fresh loops and must both complete successfully. + assert greet("World") == "Hello, World!" + assert greet("Temporal") == "Hello, Temporal!" + + # Keep the test environment's event loop available while the synchronous + # caller blocks waiting for the workflow result on its own thread. + await asyncio.to_thread(call_from_sync_code)