Skip to content

Commit 658d2e1

Browse files
committed
Experimental: AWS S3 driver extra package
1 parent 83738c3 commit 658d2e1

11 files changed

Lines changed: 2270 additions & 29 deletions

File tree

README.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ informal introduction to the features and their implementation.
5656
- [Custom Type Data Conversion](#custom-type-data-conversion)
5757
- [External Storage](#external-storage)
5858
- [Driver Selection](#driver-selection)
59+
- [Built-in Drivers](#built-in-drivers)
5960
- [Custom Drivers](#custom-drivers)
6061
- [Workers](#workers)
6162
- [Workflows](#workflows)
@@ -467,23 +468,32 @@ External storage allows large payloads to be offloaded to an external storage se
467468

468469
External storage is configured via the `external_storage` parameter on `DataConverter`. It should be configured on the `Client` both for clients of your workflow as well as on the worker -- anywhere large payloads may be uploaded or downloaded.
469470

470-
A `StorageDriver` handles uploading and downloading payloads. Temporal provides built-in drivers for common storage solutions, or you may customize one. Here's an example using our provided `InMemoryTestDriver`.
471+
A `StorageDriver` handles uploading and downloading payloads. Temporal provides [built-in drivers](#built-in-drivers) for common storage solutions, or you may implement a [custom driver](#custom-drivers). Here's an example using the built-in `S3StorageDriver`.
471472

472473
```python
474+
import aioboto3
473475
import dataclasses
474-
from temporalio.client import Client
476+
from temporalio.client import Client, ClientConfig
477+
from temporalio.contrib.aws.s3driver import S3StorageDriver
475478
from temporalio.converter import DataConverter
476479
from temporalio.converter import ExternalStorage
480+
from types_aiobotocore_s3.client import S3Client
477481

478-
driver = InMemoryTestDriver()
482+
client_config = ClientConfig.load_client_connect_config()
479483

480-
client = await Client.connect(
481-
"localhost:7233",
482-
data_converter=dataclasses.replace(
483-
DataConverter.default,
484-
external_storage=ExternalStorage(drivers=[driver]),
485-
),
486-
)
484+
session = aioboto3.Session()
485+
async with session.client("s3") as s3_client:
486+
driver = S3StorageDriver(
487+
client=s3_client,
488+
bucket="my-bucket",
489+
)
490+
client = await Client.connect(
491+
**client_config,
492+
data_converter=dataclasses.replace(
493+
DataConverter.default,
494+
external_storage=ExternalStorage(drivers=[driver]),
495+
),
496+
)
487497
```
488498

489499
Some things to note about external storage:
@@ -540,6 +550,10 @@ Some things to note about driver selection:
540550
* Returning `None` from a selector leaves the payload stored inline in workflow history rather than offloading it.
541551
* The driver instance returned by the selector must be one of the instances registered in `ExternalStorage.drivers`. If it is not, an error is raised.
542552

553+
###### Built-in Drivers
554+
555+
- **[S3 Storage Driver](temporalio/contrib/aws/s3driver/)**: ⚠️ **Experimental** ⚠️ Amazon S3 driver. Install dependencies with `pip install "temporalio[aws-s3]"`.
556+
543557
###### Custom Drivers
544558

545559
Implement `temporalio.converter.StorageDriver` to integrate with an external storage system:

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ opentelemetry = ["opentelemetry-api>=1.11.1,<2", "opentelemetry-sdk>=1.11.1,<2"]
3030
pydantic = ["pydantic>=2.0.0,<3"]
3131
openai-agents = ["openai-agents>=0.3,<0.7", "mcp>=1.9.4, <2"]
3232
google-adk = ["google-adk>=1.27.0,<2"]
33+
aws-s3 = [
34+
"aioboto3>=10.4.0",
35+
"types-aioboto3[s3]>=10.4.0",
36+
]
3337

3438
[project.urls]
3539
Homepage = "https://github.com/temporalio/sdk-python"
@@ -64,6 +68,7 @@ dev = [
6468
"openinference-instrumentation-google-adk>=0.1.8",
6569
"googleapis-common-protos==1.70.0",
6670
"pytest-rerunfailures>=16.1",
71+
"moto[s3,server]>=5",
6772
]
6873

6974
[tool.poe.tasks]

temporalio/contrib/aws/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# AWS Integrations for Temporal Python SDK
2+
3+
This directory contains AWS service integrations for the Temporal Python SDK.
4+
5+
## Integrations
6+
7+
- **[S3 Storage Driver](s3driver/)**: ⚠️ **Experimental** ⚠️ Amazon S3 driver for [external storage](../../../README.md#external-storage). Install dependencies with `pip install "temporalio[aws-s3]"`.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# AWS Integration for Temporal Python SDK
2+
3+
> ⚠️ **This package is currently at an experimental release stage.** ⚠️
4+
5+
This package provides AWS integrations for the Temporal Python SDK, including an Amazon S3 driver for [external storage](../../../README.md#external-storage).
6+
7+
## Install Dependencies
8+
9+
python -m pip install "temporalio[aws-s3]"
10+
11+
## S3 Driver
12+
13+
`temporalio.contrib.aws.s3driver.S3StorageDriver` stores and retrieves Temporal payloads in Amazon S3. It requires an [`aioboto3`](https://github.com/terrycain/aioboto3) S3 client and a `bucket` — either a static name or a callable for dynamic per-payload selection.
14+
15+
```python
16+
import aioboto3
17+
import dataclasses
18+
from temporalio.client import Client
19+
from temporalio.contrib.aws.s3driver import S3StorageDriver
20+
from temporalio.converter import DataConverter, ExternalStorage
21+
22+
session = aioboto3.Session()
23+
# Credentials and region are resolved automatically from the standard AWS credential
24+
# chain e.g. environment variables, ~/.aws/config, IAM instance profile, and so on.
25+
async with session.client("s3") as s3_client:
26+
driver = S3StorageDriver(client=s3_client, bucket="my-temporal-payloads")
27+
28+
client = await Client.connect(
29+
"localhost:7233",
30+
data_converter=dataclasses.replace(
31+
DataConverter.default,
32+
external_storage=ExternalStorage(drivers=[driver]),
33+
),
34+
)
35+
```
36+
37+
Payloads are stored under content-addressable keys derived from a SHA-256 hash of the serialized payload bytes, segmented by namespace and workflow/activity identifiers when serialization context is available, e.g.:
38+
39+
v0/ns/my-namespace/wfi/my-workflow-id/d/sha256/<hash>
40+
41+
Some things to note about the S3 driver:
42+
43+
* Any driver used to store payloads must also be configured on the component that retrieves them. If the client stores workflow inputs using this driver, the worker must include it in its `ExternalStorage.drivers` list to retrieve them.
44+
* Credentials, region, endpoint, and other AWS settings are configured on the `aioboto3` client directly.
45+
* The target S3 bucket must already exist; the driver will not create it.
46+
* Identical serialized bytes within the same namespace and workflow (or activity) share the same S3 object — the key is content-addressable within that scope. The same bytes used across different workflows or namespaces produce distinct S3 objects because the key includes the namespace and workflow/activity identifiers.
47+
* Only payloads at or above `ExternalStorage.payload_size_threshold` (default: 256 KiB) are offloaded; smaller payloads are stored inline. Set `payload_size_threshold=None` to offload every payload regardless of size.
48+
* `max_payload_size` (default: 50 MiB) sets a hard upper limit on the serialized size of any single payload. A `ValueError` is raised at store time if a payload exceeds this limit. Increase it if your workflows produce payloads larger than 50 MiB.
49+
* Override `driver_name` only when registering multiple `S3StorageDriver` instances with distinct configurations under the same `ExternalStorage.drivers` list.
50+
51+
### Dynamic Bucket Selection
52+
53+
To select the S3 bucket per payload, pass a callable as `bucket`:
54+
55+
```python
56+
from temporalio.contrib.aws.s3driver import S3StorageDriver
57+
58+
driver = S3StorageDriver(
59+
client=s3_client,
60+
bucket=lambda context, payload: (
61+
"large-payloads" if payload.ByteSize() > 10 * 1024 * 1024 else "small-payloads"
62+
),
63+
)
64+
```
65+
66+
### Required IAM permissions
67+
68+
The AWS credentials used by the `aioboto3` client must have the following S3 permissions on the target bucket and its objects:
69+
70+
```json
71+
{
72+
"Effect": "Allow",
73+
"Action": [
74+
"s3:PutObject",
75+
"s3:GetObject"
76+
],
77+
"Resource": "arn:aws:s3:::my-temporal-payloads/*"
78+
}
79+
```
80+
81+
`s3:PutObject` is required by components that store payloads (typically the Temporal client and worker sending workflow/activity inputs), and `s3:GetObject` is required by components that retrieve them (typically workers and clients reading results). Components that only retrieve payloads do not need `s3:PutObject`, and vice versa.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Amazon S3 storage driver for Temporal external payload storage."""
2+
3+
from temporalio.contrib.aws.s3driver._client import (
4+
S3StorageDriverClient,
5+
new_aioboto3_client,
6+
)
7+
from temporalio.contrib.aws.s3driver._driver import S3StorageDriver
8+
9+
__all__ = [
10+
"S3StorageDriverClient",
11+
"S3StorageDriver",
12+
"new_aioboto3_client",
13+
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""S3 storage driver client abstraction for the S3 storage driver.
2+
3+
.. warning::
4+
This API is experimental.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import io
10+
from abc import ABC, abstractmethod
11+
12+
from types_aiobotocore_s3.client import S3Client
13+
14+
15+
class S3StorageDriverClient(ABC):
16+
"""Abstract base class for S3 object operations.
17+
18+
Implementations must support ``put_object`` and ``get_object``. Multipart
19+
upload handling (if needed) is an internal concern of each implementation.
20+
21+
.. warning::
22+
This API is experimental.
23+
"""
24+
25+
@abstractmethod
26+
async def put_object(self, *, bucket: str, key: str, data: bytes) -> None:
27+
"""Upload *data* to the given S3 *bucket* and *key*."""
28+
29+
@abstractmethod
30+
async def get_object(self, *, bucket: str, key: str) -> bytes:
31+
"""Download and return the bytes stored at the given S3 *bucket* and *key*."""
32+
33+
34+
class Aioboto3StorageDriverClient(S3StorageDriverClient):
35+
"""Adapter that wraps an aioboto3 S3 client as an :class:`S3StorageDriverClient`.
36+
37+
Internally delegates to ``upload_fileobj`` for uploads (which handles
38+
multipart automatically for objects above the multipart threshold) and
39+
``get_object`` for downloads.
40+
41+
.. warning::
42+
This API is experimental.
43+
"""
44+
45+
def __init__(self, client: S3Client) -> None:
46+
"""Wrap an aioboto3 S3 client.
47+
48+
Args:
49+
client: An aioboto3 S3 client, typically obtained from
50+
``aioboto3.Session().client("s3")``.
51+
"""
52+
self._client = client
53+
54+
async def put_object(self, *, bucket: str, key: str, data: bytes) -> None:
55+
"""Upload *data* via aioboto3's ``upload_fileobj``."""
56+
# upload_fileobj is an aioboto3-specific method not in the
57+
# types_aiobotocore_s3 stubs; it handles multipart automatically.
58+
await self._client.upload_fileobj(io.BytesIO(data), bucket, key) # type: ignore[arg-type]
59+
60+
async def get_object(self, *, bucket: str, key: str) -> bytes:
61+
"""Download bytes via aioboto3's ``get_object``."""
62+
response = await self._client.get_object(Bucket=bucket, Key=key)
63+
# StreamingBody.read() is untyped in aiobotocore, returns bytes at runtime.
64+
return await response["Body"].read() # type: ignore[no-any-return]
65+
66+
67+
def new_aioboto3_client(client: S3Client) -> Aioboto3StorageDriverClient:
68+
"""Create an :class:`S3StorageDriverClient` from an aioboto3 S3 client.
69+
70+
This is a convenience factory. Equivalent to ``Aioboto3StorageDriverClient(client)``.
71+
72+
Args:
73+
client: An aioboto3 S3 client, typically obtained from
74+
``aioboto3.Session().client("s3")``.
75+
"""
76+
return Aioboto3StorageDriverClient(client)

0 commit comments

Comments
 (0)