Skip to content

Commit b2d7c1a

Browse files
committed
Re-add BlobPayloadStoreOptions
1 parent ed8718f commit b2d7c1a

File tree

10 files changed

+106
-74
lines changed

10 files changed

+106
-74
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ ADDED
1515
Pass a `BlobPayloadStore` to the worker and client via the
1616
`payload_store` parameter.
1717
- Added `durabletask.extensions.azure_blob_payloads` extension
18-
package with `BlobPayloadStore`
18+
package with `BlobPayloadStore` and `BlobPayloadStoreOptions`
1919
- Added `PayloadStore` abstract base class in
2020
`durabletask.payload` for custom storage backends
2121
- Added `durabletask.testing` module with `InMemoryOrchestrationBackend` for testing orchestrations without a sidecar process

docs/features.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,15 @@ The built-in `BlobPayloadStore` uses Azure Blob Storage. Create a
186186
store instance and pass it to both the worker and client:
187187

188188
```python
189-
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore
189+
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions
190190

191-
store = BlobPayloadStore(
191+
store = BlobPayloadStore(BlobPayloadStoreOptions(
192192
connection_string="DefaultEndpointsProtocol=https;...",
193193
container_name="durabletask-payloads", # default
194194
threshold_bytes=900_000, # default (900 KB)
195195
max_stored_payload_bytes=10_485_760, # default (10 MB)
196196
enable_compression=True, # default
197-
)
197+
))
198198
```
199199

200200
Then pass the store to the worker and client:
@@ -225,10 +225,10 @@ You can also authenticate using `account_url` and a
225225
```python
226226
from azure.identity import DefaultAzureCredential
227227

228-
store = BlobPayloadStore(
228+
store = BlobPayloadStore(BlobPayloadStoreOptions(
229229
account_url="https://<account>.blob.core.windows.net",
230230
credential=DefaultAzureCredential(),
231-
)
231+
))
232232
```
233233

234234
#### Configuration options

docs/supported-patterns.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,14 @@ install the optional dependency and configure a payload store on the
133133
worker and client:
134134

135135
```python
136-
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore
136+
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions
137137
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
138138
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
139139

140140
# Configure the blob payload store
141-
store = BlobPayloadStore(
141+
store = BlobPayloadStore(BlobPayloadStoreOptions(
142142
connection_string="DefaultEndpointsProtocol=https;...",
143-
)
143+
))
144144

145145
# Pass the store to both worker and client
146146
with DurableTaskSchedulerWorker(

durabletask/extensions/azure_blob_payloads/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
1414
Usage::
1515
16-
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore
16+
from durabletask.extensions.azure_blob_payloads import (
17+
BlobPayloadStore,
18+
BlobPayloadStoreOptions,
19+
)
1720
18-
store = BlobPayloadStore(
21+
store = BlobPayloadStore(BlobPayloadStoreOptions(
1922
connection_string="DefaultEndpointsProtocol=https;...",
20-
)
23+
))
2124
worker = TaskHubGrpcWorker(payload_store=store)
2225
"""
2326

@@ -30,5 +33,6 @@
3033
) from exc
3134

3235
from durabletask.extensions.azure_blob_payloads.blob_payload_store import BlobPayloadStore
36+
from durabletask.extensions.azure_blob_payloads.options import BlobPayloadStoreOptions
3337

34-
__all__ = ["BlobPayloadStore"]
38+
__all__ = ["BlobPayloadStore", "BlobPayloadStoreOptions"]

durabletask/extensions/azure_blob_payloads/blob_payload_store.py

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import gzip
99
import logging
1010
import uuid
11-
from typing import Any, Optional
11+
from typing import Optional
1212

1313
from azure.storage.blob import BlobServiceClient
1414
from azure.storage.blob.aio import BlobServiceClient as AsyncBlobServiceClient
1515

16-
from durabletask.payload.store import LargePayloadStorageOptions, PayloadStore
16+
from durabletask.extensions.azure_blob_payloads.options import BlobPayloadStoreOptions
17+
from durabletask.payload.store import PayloadStore
1718

1819
logger = logging.getLogger("durabletask-blobpayloads")
1920

@@ -29,79 +30,60 @@ class BlobPayloadStore(PayloadStore):
2930
token format (``blob:v1:<container>:<blobName>``) and the same
3031
storage layout, allowing cross-language interoperability.
3132
33+
Example::
34+
35+
store = BlobPayloadStore(BlobPayloadStoreOptions(
36+
connection_string="...",
37+
))
38+
3239
Args:
33-
connection_string: Azure Storage connection string. Mutually
34-
exclusive with *account_url*.
35-
account_url: Azure Storage account URL. Must be combined with
36-
*credential*.
37-
credential: A ``TokenCredential`` for token-based auth.
38-
container_name: Blob container for externalized payloads.
39-
threshold_bytes: Payloads larger than this are externalized.
40-
max_stored_payload_bytes: Maximum externalized payload size.
41-
enable_compression: GZip-compress payloads before uploading.
42-
api_version: Azure Storage API version override (useful for
43-
Azurite compatibility).
40+
options: A :class:`BlobPayloadStoreOptions` with all settings.
4441
"""
4542

46-
def __init__(
47-
self,
48-
*,
49-
connection_string: Optional[str] = None,
50-
account_url: Optional[str] = None,
51-
credential: Any = None,
52-
container_name: str = "durabletask-payloads",
53-
threshold_bytes: int = 900_000,
54-
max_stored_payload_bytes: int = 10 * 1024 * 1024,
55-
enable_compression: bool = True,
56-
api_version: Optional[str] = None,
57-
):
58-
if not connection_string and not account_url:
43+
def __init__(self, options: BlobPayloadStoreOptions):
44+
if not options.connection_string and not options.account_url:
5945
raise ValueError(
6046
"Either 'connection_string' or 'account_url' (with 'credential') must be provided."
6147
)
6248

63-
self._options = LargePayloadStorageOptions(
64-
threshold_bytes=threshold_bytes,
65-
max_stored_payload_bytes=max_stored_payload_bytes,
66-
enable_compression=enable_compression,
67-
)
68-
self._container_name = container_name
49+
self._options = options
50+
self._container_name = options.container_name
6951

7052
# Optional kwargs shared by both sync and async clients.
7153
extra_kwargs: dict = {}
72-
if api_version:
73-
extra_kwargs["api_version"] = api_version
54+
if options.api_version:
55+
extra_kwargs["api_version"] = options.api_version
7456

7557
# Build sync client
76-
if connection_string:
58+
if options.connection_string:
7759
self._blob_service_client = BlobServiceClient.from_connection_string(
78-
connection_string, **extra_kwargs,
60+
options.connection_string, **extra_kwargs,
7961
)
8062
else:
81-
assert account_url is not None # guaranteed by validation above
63+
assert options.account_url is not None # guaranteed by validation above
8264
self._blob_service_client = BlobServiceClient(
83-
account_url=account_url,
84-
credential=credential,
65+
account_url=options.account_url,
66+
credential=options.credential,
8567
**extra_kwargs,
8668
)
8769

8870
# Build async client
89-
if connection_string:
71+
if options.connection_string:
9072
self._async_blob_service_client = AsyncBlobServiceClient.from_connection_string(
91-
connection_string, **extra_kwargs,
73+
options.connection_string, **extra_kwargs,
9274
)
9375
else:
94-
assert account_url is not None # guaranteed by validation above
76+
assert options.account_url is not None # guaranteed by validation above
9577
self._async_blob_service_client = AsyncBlobServiceClient(
96-
account_url=account_url,
97-
credential=credential,
78+
account_url=options.account_url,
79+
credential=options.credential,
9880
**extra_kwargs,
9981
)
10082

10183
self._ensure_container_created = False
10284

10385
@property
104-
def options(self) -> LargePayloadStorageOptions:
86+
def options(self) -> BlobPayloadStoreOptions:
10587
return self._options
10688

10789
# ------------------------------------------------------------------
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Configuration options for the Azure Blob payload store."""
5+
6+
from __future__ import annotations
7+
8+
from dataclasses import dataclass, field
9+
from typing import Any, Optional
10+
11+
from durabletask.payload.store import LargePayloadStorageOptions
12+
13+
14+
@dataclass
15+
class BlobPayloadStoreOptions(LargePayloadStorageOptions):
16+
"""Configuration specific to the Azure Blob payload store.
17+
18+
Inherits general threshold / compression settings from
19+
:class:`~durabletask.payload.store.LargePayloadStorageOptions`
20+
and adds Azure Blob-specific fields.
21+
22+
Attributes:
23+
container_name: Azure Blob container used to store externalized
24+
payloads. Defaults to ``"durabletask-payloads"``.
25+
connection_string: Azure Storage connection string. Mutually
26+
exclusive with *account_url*.
27+
account_url: Azure Storage account URL (e.g.
28+
``"https://<account>.blob.core.windows.net"``). Use
29+
together with *credential* for token-based auth.
30+
credential: A ``TokenCredential`` instance (e.g.
31+
``DefaultAzureCredential``) for authenticating to the
32+
storage account when using *account_url*.
33+
api_version: Azure Storage API version override (useful for
34+
Azurite compatibility).
35+
"""
36+
container_name: str = "durabletask-payloads"
37+
connection_string: Optional[str] = None
38+
account_url: Optional[str] = None
39+
credential: Any = field(default=None, repr=False)
40+
api_version: Optional[str] = None

examples/large_payload/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ $env:STORAGE_CONNECTION_STRING = "DefaultEndpointsProtocol=https;..."
102102

103103
## Configuration Options
104104

105-
The `BlobPayloadStore` constructor supports the following settings:
105+
The `BlobPayloadStoreOptions` class supports the following settings:
106106

107107
| Option | Default | Description |
108108
|---|---|---|

examples/large_payload/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from durabletask import client, task
3131
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
3232
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
33-
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore
33+
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions
3434

3535

3636
# --------------- Activities ---------------
@@ -77,11 +77,11 @@ def main():
7777
print(f"Using endpoint: {endpoint}")
7878

7979
# Configure the blob payload store
80-
store = BlobPayloadStore(
80+
store = BlobPayloadStore(BlobPayloadStoreOptions(
8181
connection_string=storage_conn_str,
8282
# Use a low threshold so that we can see externalization in action
8383
threshold_bytes=1_024,
84-
)
84+
))
8585

8686
secure_channel = endpoint.startswith("https://")
8787
credential = DefaultAzureCredential() if secure_channel else None

tests/durabletask/test_large_payload.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -417,24 +417,30 @@ class TestBlobPayloadStoreDefaults:
417417
def test_default_options(self):
418418
"""Constructing with connection_string should use .NET SDK defaults."""
419419
pytest.importorskip("azure.storage.blob")
420-
from durabletask.extensions.azure_blob_payloads.blob_payload_store import BlobPayloadStore
420+
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions
421421

422-
store = BlobPayloadStore(connection_string="UseDevelopmentStorage=true")
422+
store = BlobPayloadStore(BlobPayloadStoreOptions(
423+
connection_string="UseDevelopmentStorage=true",
424+
))
423425
opts = store.options
424426
assert opts.threshold_bytes == 900_000
425427
assert opts.max_stored_payload_bytes == 10 * 1024 * 1024
426428
assert opts.enable_compression is True
429+
assert opts.container_name == "durabletask-payloads"
430+
assert opts.connection_string == "UseDevelopmentStorage=true"
427431

428432
def test_custom_options(self):
429433
"""Custom constructor params should be reflected in options."""
430434
pytest.importorskip("azure.storage.blob")
431-
from durabletask.extensions.azure_blob_payloads.blob_payload_store import BlobPayloadStore
435+
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions
432436

433-
store = BlobPayloadStore(
437+
store = BlobPayloadStore(BlobPayloadStoreOptions(
434438
connection_string="UseDevelopmentStorage=true",
435439
threshold_bytes=500_000,
436-
)
440+
container_name="my-container",
441+
))
437442
assert store.options.threshold_bytes == 500_000
443+
assert store.options.container_name == "my-container"
438444

439445

440446
# ------------------------------------------------------------------
@@ -549,5 +555,5 @@ def test_no_credentials_raises(self):
549555
pytest.importorskip("azure.storage.blob")
550556
from durabletask.extensions.azure_blob_payloads.blob_payload_store import BlobPayloadStore
551557

552-
with pytest.raises(ValueError, match="Either 'connection_string' or 'account_url'"):
553-
BlobPayloadStore()
558+
with pytest.raises(TypeError):
559+
BlobPayloadStore() # type: ignore[call-arg]

tests/durabletask/test_large_payload_e2e.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# Skip the entire module if azure-storage-blob is not installed.
2626
azure_blob = pytest.importorskip("azure.storage.blob")
2727

28-
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore # noqa: E402
28+
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions # noqa: E402
2929

3030
# Azurite well-known connection string
3131
AZURITE_CONN_STR = "UseDevelopmentStorage=true"
@@ -76,13 +76,13 @@ def _azurite_is_running() -> bool:
7676
@pytest.fixture(scope="module")
7777
def payload_store():
7878
"""Create a BlobPayloadStore pointing at Azurite with a low threshold."""
79-
store = BlobPayloadStore(
79+
store = BlobPayloadStore(BlobPayloadStoreOptions(
8080
connection_string=AZURITE_CONN_STR,
8181
container_name=TEST_CONTAINER,
8282
threshold_bytes=THRESHOLD_BYTES,
8383
enable_compression=True,
8484
api_version=AZURITE_API_VERSION,
85-
)
85+
))
8686
yield store
8787

8888
# Clean up: delete the test container.
@@ -318,13 +318,13 @@ def test_small_payload_stays_inline(self, payload_store):
318318

319319
# Use a fresh container to isolate blob count
320320
fresh_container = f"small-test-{uuid.uuid4().hex[:8]}"
321-
store = BlobPayloadStore(
321+
store = BlobPayloadStore(BlobPayloadStoreOptions(
322322
connection_string=AZURITE_CONN_STR,
323323
container_name=fresh_container,
324324
threshold_bytes=THRESHOLD_BYTES,
325325
enable_compression=True,
326326
api_version=AZURITE_API_VERSION,
327-
)
327+
))
328328

329329
def echo(ctx: task.OrchestrationContext, inp: str):
330330
return inp

0 commit comments

Comments
 (0)