Skip to content

Commit 7ed18f3

Browse files
author
Alex Wang
committed
fix: fix datetime serilization for lambda invoker
- Fix datetime serilization for lambda invoker - Refactor serilization for filesystem and web module - Remove unused import - Remove unused txt file - More unit tests for web/server
1 parent 318bc19 commit 7ed18f3

8 files changed

Lines changed: 127 additions & 57 deletions

File tree

src/aws_durable_execution_sdk_python_testing/invoker.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
DurableExecutionInvocationInputWithClient,
1313
DurableExecutionInvocationOutput,
1414
InitialExecutionState,
15-
InvocationStatus,
1615
)
1716

1817
from aws_durable_execution_sdk_python_testing.exceptions import (
1918
DurableFunctionsTestError,
20-
ServiceException,
2119
)
2220
from aws_durable_execution_sdk_python_testing.model import LambdaContext
21+
from aws_durable_execution_sdk_python_testing.serialization import SimthyDateTimeEncoder
2322

2423
if TYPE_CHECKING:
2524
from collections.abc import Callable
@@ -239,7 +238,7 @@ def invoke(
239238
response = client.invoke(
240239
FunctionName=function_name,
241240
InvocationType="RequestResponse", # Synchronous invocation
242-
Payload=json.dumps(input.to_dict(), default=str),
241+
Payload=json.dumps(input.to_dict(), cls=SimthyDateTimeEncoder),
243242
)
244243

245244
# Check HTTP status code

src/aws_durable_execution_sdk_python_testing/web/serialization.py renamed to src/aws_durable_execution_sdk_python_testing/serialization.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66

77
from __future__ import annotations
88

9-
import json
109
import os
10+
import json
1111
from typing import Any, Protocol
12-
from datetime import datetime
12+
from datetime import datetime, UTC
1313

14-
import aws_durable_execution_sdk_python
1514
import botocore.loaders # type: ignore
1615
from botocore.model import ServiceModel # type: ignore
1716
from botocore.parsers import create_parser # type: ignore
@@ -22,6 +21,40 @@
2221
)
2322

2423

24+
class DateTimeEncoder(json.JSONEncoder):
25+
"""Custom JSON encoder that handles datetime objects."""
26+
27+
def default(self, obj):
28+
if isinstance(obj, datetime):
29+
return obj.timestamp()
30+
return super().default(obj)
31+
32+
33+
class SimthyDateTimeEncoder(json.JSONEncoder):
34+
"""Custom JSON encoder that converts datetime objects to millisecond timestamps, which match smithy encoding behaviour"""
35+
36+
def default(self, obj):
37+
if isinstance(obj, datetime):
38+
# seconds_float to milliseconds
39+
return int(obj.timestamp() * 1000)
40+
return super().default(obj)
41+
42+
43+
def datetime_object_hook(obj):
44+
"""JSON object hook to convert unix timestamps back to datetime objects."""
45+
if isinstance(obj, dict):
46+
for key, value in obj.items():
47+
if isinstance(value, int | float) and key.endswith(
48+
("_timestamp", "_time", "Timestamp", "Time")
49+
):
50+
try: # noqa: SIM105
51+
obj[key] = datetime.fromtimestamp(value, tz=UTC)
52+
except (ValueError, OSError):
53+
# Leave as number if not a valid timestamp
54+
pass
55+
return obj
56+
57+
2558
class Serializer(Protocol):
2659
"""Interface for serializing data to bytes."""
2760

@@ -64,22 +97,13 @@ class JSONSerializer:
6497
def to_bytes(self, data: Any) -> bytes:
6598
"""Serialize data to JSON bytes."""
6699
try:
67-
json_string = json.dumps(
68-
data, separators=(",", ":"), default=self._default_handler
69-
)
100+
json_string = json.dumps(data, separators=(",", ":"), cls=DateTimeEncoder)
70101
return json_string.encode("utf-8")
71102
except (TypeError, ValueError) as e:
72103
raise InvalidParameterValueException(
73104
f"Failed to serialize data to JSON: {str(e)}"
74105
)
75106

76-
def _default_handler(self, obj: Any) -> float:
77-
"""Handle non-permitive objects."""
78-
if isinstance(obj, datetime):
79-
return obj.timestamp()
80-
# Raise TypeError for unsupported types
81-
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
82-
83107

84108
class AwsRestJsonSerializer:
85109
"""AWS rest-json serializer using boto."""

src/aws_durable_execution_sdk_python_testing/stores/filesystem.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,21 @@
44

55
import json
66
import logging
7-
from datetime import UTC, datetime
87
from pathlib import Path
98

109
from aws_durable_execution_sdk_python_testing.exceptions import (
1110
ResourceNotFoundException,
1211
)
12+
from aws_durable_execution_sdk_python_testing.serialization import (
13+
DateTimeEncoder,
14+
datetime_object_hook,
15+
)
1316
from aws_durable_execution_sdk_python_testing.execution import Execution
1417
from aws_durable_execution_sdk_python_testing.stores.base import (
1518
BaseExecutionStore,
1619
)
1720

1821

19-
class DateTimeEncoder(json.JSONEncoder):
20-
"""Custom JSON encoder that handles datetime objects."""
21-
22-
def default(self, obj):
23-
if isinstance(obj, datetime):
24-
return obj.timestamp()
25-
return super().default(obj)
26-
27-
28-
def datetime_object_hook(obj):
29-
"""JSON object hook to convert unix timestamps back to datetime objects."""
30-
if isinstance(obj, dict):
31-
for key, value in obj.items():
32-
if isinstance(value, int | float) and key.endswith(
33-
("_timestamp", "_time", "Timestamp", "Time")
34-
):
35-
try: # noqa: SIM105
36-
obj[key] = datetime.fromtimestamp(value, tz=UTC)
37-
except (ValueError, OSError):
38-
# Leave as number if not a valid timestamp
39-
pass
40-
return obj
41-
42-
4322
class FileSystemExecutionStore(BaseExecutionStore):
4423
"""File system-based execution store for persistence."""
4524

src/aws_durable_execution_sdk_python_testing/stores/sqlite.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from aws_durable_execution_sdk_python_testing.stores.base import (
1818
ExecutionStore,
1919
)
20-
from aws_durable_execution_sdk_python_testing.stores.filesystem import (
20+
from aws_durable_execution_sdk_python_testing.serialization import (
2121
DateTimeEncoder,
2222
datetime_object_hook,
2323
)

src/aws_durable_execution_sdk_python_testing/web/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
# Removed deprecated imports from web.errors
1616
from aws_durable_execution_sdk_python_testing.web.routes import Route
17-
from aws_durable_execution_sdk_python_testing.web.serialization import (
17+
from aws_durable_execution_sdk_python_testing.serialization import (
1818
AwsRestJsonDeserializer,
1919
JSONSerializer,
2020
Serializer,

tests/how-to-run-from-term.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

tests/invoker_test.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
InvocationStatus,
1313
)
1414

15+
from aws_durable_execution_sdk_python.lambda_service import (
16+
ExecutionDetails,
17+
Operation,
18+
OperationStatus,
19+
OperationType,
20+
)
21+
22+
from aws_durable_execution_sdk_python_testing.serialization import SimthyDateTimeEncoder
23+
24+
from datetime import datetime, UTC
25+
1526
from aws_durable_execution_sdk_python_testing.execution import Execution
1627
from aws_durable_execution_sdk_python_testing.invoker import (
1728
InProcessInvoker,
@@ -168,10 +179,23 @@ def test_lambda_invoker_invoke_success():
168179

169180
invoker = LambdaInvoker(lambda_client)
170181

182+
mock_operation = Operation(
183+
operation_id="op-1",
184+
parent_id=None,
185+
name="test-execution",
186+
start_timestamp=datetime.now(UTC),
187+
end_timestamp=datetime.now(UTC),
188+
operation_type=OperationType.EXECUTION,
189+
status=OperationStatus.SUCCEEDED,
190+
execution_details=ExecutionDetails(input_payload='{"test": "data"}'),
191+
)
192+
171193
input_data = DurableExecutionInvocationInput(
172194
durable_execution_arn="test-arn",
173195
checkpoint_token="test-token", # noqa: S106
174-
initial_execution_state=InitialExecutionState(operations=[], next_marker=""),
196+
initial_execution_state=InitialExecutionState(
197+
operations=[mock_operation], next_marker=""
198+
),
175199
)
176200

177201
response = invoker.invoke("test-function", input_data)
@@ -185,7 +209,7 @@ def test_lambda_invoker_invoke_success():
185209
lambda_client.invoke.assert_called_once_with(
186210
FunctionName="test-function",
187211
InvocationType="RequestResponse",
188-
Payload=json.dumps(input_data.to_dict(), default=str),
212+
Payload=json.dumps(input_data.to_dict(), cls=SimthyDateTimeEncoder),
189213
)
190214

191215

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
from aws_durable_execution_sdk_python_testing.exceptions import (
1212
InvalidParameterValueException,
1313
)
14-
from aws_durable_execution_sdk_python_testing.web.serialization import (
14+
from aws_durable_execution_sdk_python_testing.serialization import (
1515
JSONSerializer,
1616
AwsRestJsonDeserializer,
1717
AwsRestJsonSerializer,
18+
SimthyDateTimeEncoder,
1819
)
1920

2021

@@ -40,12 +41,12 @@ def test_aws_rest_json_serializer_should_initialize_and_serialize_data():
4041
)
4142

4243

43-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.create_serializer")
44-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.ServiceModel")
44+
@patch("aws_durable_execution_sdk_python_testing.serialization.create_serializer")
45+
@patch("aws_durable_execution_sdk_python_testing.serialization.ServiceModel")
4546
@patch(
46-
"aws_durable_execution_sdk_python_testing.web.serialization.botocore.loaders.Loader"
47+
"aws_durable_execution_sdk_python_testing.serialization.botocore.loaders.Loader"
4748
)
48-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.os.path.dirname")
49+
@patch("aws_durable_execution_sdk_python_testing.serialization.os.path.dirname")
4950
def test_aws_rest_json_serializer_should_create_serializer_with_boto_components(
5051
mock_dirname,
5152
mock_loader_class,
@@ -90,7 +91,7 @@ def test_aws_rest_json_serializer_should_create_serializer_with_boto_components(
9091
mock_service_model.operation_model.assert_called_once_with(operation_name)
9192

9293

93-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.create_serializer")
94+
@patch("aws_durable_execution_sdk_python_testing.serialization.create_serializer")
9495
def test_aws_rest_json_serializer_should_raise_serialization_error_when_create_fails(
9596
mock_create_serializer,
9697
):
@@ -222,12 +223,12 @@ def test_aws_rest_json_deserializer_should_initialize_and_deserialize_data():
222223
mock_parser.parse.assert_called_once_with(expected_response_dict, mock_output_shape)
223224

224225

225-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.create_parser")
226-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.ServiceModel")
226+
@patch("aws_durable_execution_sdk_python_testing.serialization.create_parser")
227+
@patch("aws_durable_execution_sdk_python_testing.serialization.ServiceModel")
227228
@patch(
228-
"aws_durable_execution_sdk_python_testing.web.serialization.botocore.loaders.Loader"
229+
"aws_durable_execution_sdk_python_testing.serialization.botocore.loaders.Loader"
229230
)
230-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.os.path.dirname")
231+
@patch("aws_durable_execution_sdk_python_testing.serialization.os.path.dirname")
231232
def test_aws_rest_json_deserializer_should_create_deserializer_with_boto_components(
232233
mock_dirname,
233234
mock_loader_class,
@@ -274,7 +275,7 @@ def test_aws_rest_json_deserializer_should_create_deserializer_with_boto_compone
274275
mock_service_model.operation_model.assert_called_once_with(operation_name)
275276

276277

277-
@patch("aws_durable_execution_sdk_python_testing.web.serialization.create_parser")
278+
@patch("aws_durable_execution_sdk_python_testing.serialization.create_parser")
278279
def test_aws_rest_json_deserializer_should_raise_serialization_error_when_create_fails(
279280
mock_create_parser,
280281
):
@@ -574,3 +575,47 @@ def test_serialize_multiple_datetimes():
574575
expected = b'{"start":1735689600.0,"end":1767225599.0}'
575576

576577
assert result == expected
578+
579+
580+
def test_smithy_datetime_encoder_converts_datetime_to_milliseconds():
581+
"""Test that SimthyDateTimeEncoder converts datetime to millisecond timestamps."""
582+
encoder = SimthyDateTimeEncoder()
583+
dt = datetime(2025, 11, 5, 16, 30, 9, 895000, tzinfo=timezone.utc)
584+
585+
result = encoder.default(dt)
586+
expected = int(dt.timestamp() * 1000)
587+
588+
assert result == expected
589+
assert result == 1762360209895
590+
591+
592+
def test_smithy_datetime_encoder_handles_non_datetime_objects():
593+
"""Test that SimthyDateTimeEncoder delegates to parent for non-datetime objects."""
594+
encoder = SimthyDateTimeEncoder()
595+
596+
# Should delegate to parent's default method for non-datetime objects
597+
with pytest.raises(TypeError):
598+
encoder.default(object())
599+
600+
601+
def test_smithy_datetime_encoder_in_json_dumps():
602+
"""Test SimthyDateTimeEncoder when used with json.dumps."""
603+
dt = datetime(2025, 11, 5, 16, 30, 9, 895000, tzinfo=timezone.utc)
604+
data = {"timestamp": dt}
605+
606+
result = json.dumps(data, cls=SimthyDateTimeEncoder)
607+
expected = '{"timestamp": 1762360209895}'
608+
609+
assert result == expected
610+
611+
612+
def test_smithy_datetime_encoder_precision():
613+
"""Test that SimthyDateTimeEncoder maintains millisecond precision."""
614+
encoder = SimthyDateTimeEncoder()
615+
dt = datetime(2025, 11, 5, 16, 30, 9, 123456, tzinfo=timezone.utc)
616+
617+
result = encoder.default(dt)
618+
expected = int(dt.timestamp() * 1000)
619+
620+
assert result == expected
621+
assert result == 1762360209123

0 commit comments

Comments
 (0)