Skip to content

Commit 81b5098

Browse files
authored
Change minimum Python to 3.9 and add Trio sample (#162)
Fixes #161
1 parent 1e4d4e6 commit 81b5098

File tree

12 files changed

+312
-13
lines changed

12 files changed

+312
-13
lines changed

.github/workflows/ci.yml

+2-9
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,13 @@ jobs:
1212
strategy:
1313
fail-fast: true
1414
matrix:
15-
python: ["3.8", "3.12"]
15+
python: ["3.9", "3.12"]
1616
os: [ubuntu-latest, macos-intel, macos-arm, windows-latest]
1717
include:
1818
- os: macos-intel
1919
runsOn: macos-13
2020
- os: macos-arm
2121
runsOn: macos-14
22-
# macOS ARM 3.8 does not have an available Python build at
23-
# https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json.
24-
# See https://github.com/actions/setup-python/issues/808 and
25-
# https://github.com/actions/python-versions/pull/259.
26-
exclude:
27-
- os: macos-arm
28-
python: "3.8"
2922
runs-on: ${{ matrix.runsOn || matrix.os }}
3023
steps:
3124
- name: Print build information
@@ -39,7 +32,7 @@ jobs:
3932
# Using fixed Poetry version until
4033
# https://github.com/python-poetry/poetry/pull/7694 is fixed
4134
- run: python -m pip install --upgrade wheel "poetry==1.4.0" poethepoet
42-
- run: poetry install --with pydantic --with dsl --with encryption
35+
- run: poetry install --with pydantic --with dsl --with encryption --with trio_async
4336
- run: poe lint
4437
- run: mkdir junit-xml
4538
- run: poe test -s -o log_cli_level=DEBUG --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This is the set of Python samples for the [Python SDK](https://github.com/tempor
66

77
Prerequisites:
88

9-
* Python >= 3.8
9+
* Python >= 3.9
1010
* [Poetry](https://python-poetry.org)
1111
* [Temporal CLI installed](https://docs.temporal.io/cli#install)
1212
* [Local Temporal server running](https://docs.temporal.io/cli/server#start-dev)
@@ -72,6 +72,7 @@ Some examples require extra dependencies. See each sample's directory for specif
7272
* [pydantic_converter](pydantic_converter) - Data converter for using Pydantic models.
7373
* [schedules](schedules) - Demonstrates a Workflow Execution that occurs according to a schedule.
7474
* [sentry](sentry) - Report errors to Sentry.
75+
* [trio_async](trio_async) - Use asyncio Temporal in Trio-based environments.
7576
* [worker_specific_task_queues](worker_specific_task_queues) - Use unique task queues to ensure activities run on specific workers.
7677
* [worker_versioning](worker_versioning) - Use the Worker Versioning feature to more easily version your workflows & other code.
7778

poetry.lock

+65-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ packages = [
1616
"Bug Tracker" = "https://github.com/temporalio/samples-python/issues"
1717

1818
[tool.poetry.dependencies]
19-
python = "^3.8"
19+
python = "^3.9"
2020
temporalio = "^1.9.0"
2121

2222
[tool.poetry.dev-dependencies]
@@ -71,6 +71,10 @@ dependencies = { pydantic = "^1.10.4" }
7171
optional = true
7272
dependencies = { sentry-sdk = "^1.11.0" }
7373

74+
[tool.poetry.group.trio_async]
75+
optional = true
76+
dependencies = { trio = "^0.28.0", trio-asyncio = "^0.15.0" }
77+
7478
[tool.poe.tasks]
7579
format = [{cmd = "black ."}, {cmd = "isort ."}]
7680
lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }]

tests/trio_async/__init__.py

Whitespace-only changes.

tests/trio_async/workflow_test.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import uuid
2+
3+
import trio_asyncio
4+
from temporalio.client import Client
5+
from temporalio.worker import Worker
6+
7+
from trio_async import activities, workflows
8+
9+
10+
async def test_workflow_with_trio(client: Client):
11+
@trio_asyncio.aio_as_trio
12+
async def inside_trio(client: Client) -> list[str]:
13+
# Create Trio thread executor
14+
with trio_asyncio.TrioExecutor(max_workers=200) as thread_executor:
15+
task_queue = f"tq-{uuid.uuid4()}"
16+
# Run worker
17+
async with Worker(
18+
client,
19+
task_queue=task_queue,
20+
activities=[
21+
activities.say_hello_activity_async,
22+
activities.say_hello_activity_sync,
23+
],
24+
workflows=[workflows.SayHelloWorkflow],
25+
activity_executor=thread_executor,
26+
workflow_task_executor=thread_executor,
27+
):
28+
# Run workflow and return result
29+
return await client.execute_workflow(
30+
workflows.SayHelloWorkflow.run,
31+
"some-user",
32+
id=f"wf-{uuid.uuid4()}",
33+
task_queue=task_queue,
34+
)
35+
36+
result = trio_asyncio.run(inside_trio, client)
37+
assert result == [
38+
"Hello, some-user! (from asyncio)",
39+
"Hello, some-user! (from thread)",
40+
]

trio_async/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Trio Async Sample
2+
3+
This sample shows how to use Temporal asyncio with [Trio](https://trio.readthedocs.io) using
4+
[Trio asyncio](https://trio-asyncio.readthedocs.io). Specifically it demonstrates using a traditional Temporal client
5+
and worker in a Trio setting, and how Trio-based code can run in both asyncio async activities and threaded sync
6+
activities.
7+
8+
For this sample, the optional `trio_async` dependency group must be included. To include, run:
9+
10+
poetry install --with trio_async
11+
12+
To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
13+
worker:
14+
15+
poetry run python worker.py
16+
17+
This will start the worker. Then, in another terminal, run the following to execute the workflow:
18+
19+
poetry run python starter.py
20+
21+
The starter should complete with:
22+
23+
INFO:root:Workflow result: ['Hello, Temporal! (from asyncio)', 'Hello, Temporal! (from thread)']

trio_async/__init__.py

Whitespace-only changes.

trio_async/activities.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import asyncio
2+
import time
3+
4+
import trio
5+
import trio_asyncio
6+
from temporalio import activity
7+
8+
9+
# An asyncio-based async activity
10+
@activity.defn
11+
async def say_hello_activity_async(name: str) -> str:
12+
# Demonstrate a sleep in both asyncio and Trio, showing that both asyncio
13+
# and Trio primitives can be used
14+
15+
# First asyncio
16+
activity.logger.info("Sleeping in asyncio")
17+
await asyncio.sleep(0.1)
18+
19+
# Now Trio. We have to invoke the function separately decorated.
20+
# We cannot use the @trio_as_aio decorator on the activity itself because
21+
# it doesn't use functools wrap or similar so it doesn't respond to things
22+
# like __name__ that @activity.defn needs.
23+
return await say_hello_in_trio_from_asyncio(name)
24+
25+
26+
@trio_asyncio.trio_as_aio
27+
async def say_hello_in_trio_from_asyncio(name: str) -> str:
28+
activity.logger.info("Sleeping in Trio (from asyncio)")
29+
await trio.sleep(0.1)
30+
return f"Hello, {name}! (from asyncio)"
31+
32+
33+
# A thread-based sync activity
34+
@activity.defn
35+
def say_hello_activity_sync(name: str) -> str:
36+
# Demonstrate a sleep in both threaded and Trio, showing that both
37+
# primitives can be used
38+
39+
# First, thread-blocking
40+
activity.logger.info("Sleeping normally")
41+
time.sleep(0.1)
42+
43+
# Now Trio. We have to use Trio's thread sync tools to run trio calls from
44+
# a different thread.
45+
return trio.from_thread.run(say_hello_in_trio_from_sync, name)
46+
47+
48+
async def say_hello_in_trio_from_sync(name: str) -> str:
49+
activity.logger.info("Sleeping in Trio (from thread)")
50+
await trio.sleep(0.1)
51+
return f"Hello, {name}! (from thread)"

trio_async/starter.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import logging
2+
3+
import trio_asyncio
4+
from temporalio.client import Client
5+
6+
from trio_async import workflows
7+
8+
9+
@trio_asyncio.aio_as_trio # Note this decorator which allows asyncio primitives
10+
async def main():
11+
logging.basicConfig(level=logging.INFO)
12+
13+
# Connect client
14+
client = await Client.connect("localhost:7233")
15+
16+
# Execute the workflow
17+
result = await client.execute_workflow(
18+
workflows.SayHelloWorkflow.run,
19+
"Temporal",
20+
id=f"trio-async-workflow-id",
21+
task_queue="trio-async-task-queue",
22+
)
23+
logging.info(f"Workflow result: {result}")
24+
25+
26+
if __name__ == "__main__":
27+
# Note how we're using Trio event loop, not asyncio
28+
trio_asyncio.run(main)

trio_async/worker.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import asyncio
2+
import logging
3+
import os
4+
import sys
5+
6+
import trio_asyncio
7+
from temporalio.client import Client
8+
from temporalio.worker import Worker
9+
10+
from trio_async import activities, workflows
11+
12+
13+
@trio_asyncio.aio_as_trio # Note this decorator which allows asyncio primitives
14+
async def main():
15+
logging.basicConfig(level=logging.INFO)
16+
17+
# Connect client
18+
client = await Client.connect("localhost:7233")
19+
20+
# Temporal runs threaded activities and workflow tasks via run_in_executor.
21+
# Due to how trio_asyncio works, you can only do run_in_executor with their
22+
# specific executor. We make sure to give it 200 max since we are using it
23+
# for both activities and workflow tasks and by default the worker supports
24+
# 100 max concurrent activity tasks and 100 max concurrent workflow tasks.
25+
with trio_asyncio.TrioExecutor(max_workers=200) as thread_executor:
26+
27+
# Run a worker for the workflow
28+
async with Worker(
29+
client,
30+
task_queue="trio-async-task-queue",
31+
activities=[
32+
activities.say_hello_activity_async,
33+
activities.say_hello_activity_sync,
34+
],
35+
workflows=[workflows.SayHelloWorkflow],
36+
activity_executor=thread_executor,
37+
workflow_task_executor=thread_executor,
38+
):
39+
# Wait until interrupted
40+
logging.info("Worker started, ctrl+c to exit")
41+
try:
42+
await asyncio.Future()
43+
except asyncio.CancelledError:
44+
# Ignore, happens on ctrl+C
45+
pass
46+
finally:
47+
logging.info("Shutting down")
48+
49+
50+
if __name__ == "__main__":
51+
# Note how we're using Trio event loop, not asyncio
52+
try:
53+
trio_asyncio.run(main)
54+
except KeyboardInterrupt:
55+
# Ignore ctrl+c
56+
pass
57+
except BaseException as err:
58+
# On Python 3.11+ Trio represents keyboard interrupt inside an exception
59+
# group
60+
is_interrupt = (
61+
sys.version_info >= (3, 11)
62+
and isinstance(err, BaseExceptionGroup)
63+
and err.subgroup(KeyboardInterrupt)
64+
)
65+
if not is_interrupt:
66+
raise

0 commit comments

Comments
 (0)