Skip to content

Commit d2428e9

Browse files
feat: add support for custom boto3 client
1 parent 8274be2 commit d2428e9

3 files changed

Lines changed: 98 additions & 6 deletions

File tree

docs/getting-started.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,33 @@ def handler(event: dict, context: DurableContext) -> str:
228228

229229
Deploy this to Lambda and you have a durable function. The `greet_user` step is checkpointed automatically.
230230

231+
### Using a custom boto3 Lambda client
232+
233+
If you need to customize the boto3 Lambda client used for durable execution operations (for example, to configure custom endpoints, retry settings, or credentials), you can pass a `boto3_client` parameter to the decorator. The client must be a boto3 Lambda client:
234+
235+
```python
236+
import boto3
237+
from botocore.config import Config
238+
from aws_durable_execution_sdk_python import durable_execution, DurableContext
239+
240+
# Create a custom boto3 Lambda client with specific configuration
241+
custom_lambda_client = boto3.client(
242+
'lambda',
243+
config=Config(
244+
retries={'max_attempts': 5, 'mode': 'adaptive'},
245+
connect_timeout=10,
246+
read_timeout=60,
247+
)
248+
)
249+
250+
@durable_execution(boto3_client=custom_lambda_client)
251+
def handler(event: dict, context: DurableContext) -> dict:
252+
# Your durable function logic
253+
return {"status": "success"}
254+
```
255+
256+
The custom Lambda client is used for all checkpoint and state management operations. If you don't provide a `boto3_client`, the SDK initializes a default Lambda client from your environment.
257+
231258
[↑ Back to top](#table-of-contents)
232259

233260
## Next steps

src/aws_durable_execution_sdk_python/execution.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import functools
45
import json
56
import logging
67
from concurrent.futures import ThreadPoolExecutor
@@ -30,6 +31,8 @@
3031
if TYPE_CHECKING:
3132
from collections.abc import Callable, MutableMapping
3233

34+
import boto3 # type: ignore
35+
3336
from aws_durable_execution_sdk_python.types import LambdaContext
3437

3538

@@ -193,8 +196,15 @@ def create_succeeded(cls, result: str) -> DurableExecutionInvocationOutput:
193196

194197

195198
def durable_execution(
196-
func: Callable[[Any, DurableContext], Any],
199+
func: Callable[[Any, DurableContext], Any] | None = None,
200+
*,
201+
boto3_client: boto3.client | None = None, # type: ignore
197202
) -> Callable[[Any, LambdaContext], Any]:
203+
# Decorator called with parameters
204+
if func is None:
205+
logger.debug("Decorator called with parameters")
206+
return functools.partial(durable_execution, boto3_client=boto3_client)
207+
198208
logger.debug("Starting durable execution handler...")
199209

200210
def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]:
@@ -210,11 +220,15 @@ def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]:
210220
logger.debug("durableExecutionArn: %s", event.get("DurableExecutionArn"))
211221
invocation_input = DurableExecutionInvocationInput.from_dict(event)
212222

213-
service_client = (
214-
LambdaClient.initialize_local_runner_client()
215-
if invocation_input.is_local_runner
216-
else LambdaClient.initialize_from_env()
217-
)
223+
# Local runner always uses its own client, otherwise use custom or default
224+
if invocation_input.is_local_runner:
225+
service_client = LambdaClient.initialize_local_runner_client()
226+
else:
227+
service_client = (
228+
LambdaClient(client=boto3_client)
229+
if boto3_client is not None
230+
else LambdaClient.initialize_from_env()
231+
)
218232

219233
raw_input_payload: str | None = (
220234
invocation_input.initial_execution_state.get_input_payload()

tests/execution_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,3 +1961,54 @@ def test_handler(event: Any, context: DurableContext) -> dict:
19611961
assert call_args[0][0] == "Checkpoint system failed"
19621962
assert call_args[1]["extra"]["Error"] == error_obj
19631963
assert call_args[1]["extra"]["ResponseMetadata"] == metadata_obj
1964+
1965+
1966+
def test_durable_execution_with_boto3_client_parameter():
1967+
"""Test durable_execution decorator accepts boto3_client parameter."""
1968+
# GIVEN a custom boto3 Lambda client
1969+
mock_boto3_client = Mock()
1970+
mock_boto3_client.checkpoint_durable_execution.return_value = {
1971+
"CheckpointToken": "new_token",
1972+
"NewExecutionState": {"Operations": [], "NextMarker": ""},
1973+
}
1974+
mock_boto3_client.get_durable_execution_state.return_value = {
1975+
"Operations": [],
1976+
"NextMarker": "",
1977+
}
1978+
1979+
# GIVEN a durable function decorated with the custom client
1980+
@durable_execution(boto3_client=mock_boto3_client)
1981+
def test_handler(event: Any, context: DurableContext) -> dict:
1982+
return {"result": "success"}
1983+
1984+
event = {
1985+
"DurableExecutionArn": "arn:test:execution",
1986+
"CheckpointToken": "token123",
1987+
"InitialExecutionState": {
1988+
"Operations": [
1989+
{
1990+
"Id": "exec1",
1991+
"Type": "EXECUTION",
1992+
"Status": "STARTED",
1993+
"ExecutionDetails": {"InputPayload": '{"input": "test"}'},
1994+
}
1995+
],
1996+
"NextMarker": "",
1997+
},
1998+
"LocalRunner": False,
1999+
}
2000+
2001+
lambda_context = Mock()
2002+
lambda_context.aws_request_id = "test-request"
2003+
lambda_context.client_context = None
2004+
lambda_context.identity = None
2005+
lambda_context._epoch_deadline_time_in_ms = 1000000 # noqa: SLF001
2006+
lambda_context.invoked_function_arn = None
2007+
lambda_context.tenant_id = None
2008+
2009+
# WHEN the handler is invoked
2010+
result = test_handler(event, lambda_context)
2011+
2012+
# THEN the execution succeeds using the custom client
2013+
assert result["Status"] == InvocationStatus.SUCCEEDED.value
2014+
assert result["Result"] == '{"result": "success"}'

0 commit comments

Comments
 (0)