Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/aws_durable_execution_sdk_python_testing/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def to_json_dict(self) -> dict[str, Any]:
"Operations": [op.to_json_dict() for op in self.operations],
"Updates": [update.to_dict() for update in self.updates],
"InvocationCompletions": [
completion.to_dict() for completion in self.invocation_completions
completion.to_json_dict() for completion in self.invocation_completions
],
"UsedTokens": list(self.used_tokens),
"TokenSequence": self._token_sequence,
Expand Down Expand Up @@ -135,7 +135,7 @@ def from_json_dict(cls, data: dict[str, Any]) -> Execution:
OperationUpdate.from_dict(update_data) for update_data in data["Updates"]
]
execution.invocation_completions = [
InvocationCompletedDetails.from_dict(item)
InvocationCompletedDetails.from_json_dict(item)
for item in data.get("InvocationCompletions", [])
]
execution.used_tokens = set(data["UsedTokens"])
Expand Down
1 change: 1 addition & 0 deletions src/aws_durable_execution_sdk_python_testing/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def start_execution(
execution = Execution.new(input=input)
execution.start()
self._store.save(execution)
logger.debug("Created execution with ARN: %s", execution.durable_execution_arn)

completion_event = self._scheduler.create_event()
self._completion_events[execution.durable_execution_arn] = completion_event
Expand Down
7 changes: 3 additions & 4 deletions src/aws_durable_execution_sdk_python_testing/invoker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

from aws_durable_execution_sdk_python_testing.exceptions import (
DurableFunctionsTestError,
InvalidParameterValueException,
ResourceNotFoundException,
)
from aws_durable_execution_sdk_python_testing.model import LambdaContext


if TYPE_CHECKING:
from collections.abc import Callable

Expand Down Expand Up @@ -217,10 +220,6 @@ def invoke(
InvalidParameterValueException: If parameters are invalid
DurableFunctionsTestError: For other invocation failures
"""
from aws_durable_execution_sdk_python_testing.exceptions import (
ResourceNotFoundException,
InvalidParameterValueException,
)

# Parameter validation
if not function_name or not function_name.strip():
Expand Down
35 changes: 32 additions & 3 deletions src/aws_durable_execution_sdk_python_testing/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
from __future__ import annotations

import datetime
import json
from dataclasses import dataclass, replace
from enum import Enum
from typing import Any
import json

from dateutil.tz import UTC

from aws_durable_execution_sdk_python.execution import DurableExecutionInvocationOutput

Expand All @@ -30,12 +28,14 @@
OperationUpdate,
StepDetails,
StepOptions,
TimestampConverter,
WaitDetails,
WaitOptions,
)
from aws_durable_execution_sdk_python.types import (
LambdaContext as LambdaContextProtocol,
)
from dateutil.tz import UTC

from aws_durable_execution_sdk_python_testing.exceptions import (
InvalidParameterValueException,
Expand Down Expand Up @@ -1239,13 +1239,42 @@ def from_dict(cls, data: dict) -> InvocationCompletedDetails:
request_id=data["RequestId"],
)

@classmethod
def from_json_dict(cls, data: dict) -> InvocationCompletedDetails:
"""Deserialize from JSON dict with Unix millisecond timestamps."""
start_ts: datetime.datetime | None = TimestampConverter.from_unix_millis(
data["StartTimestamp"]
) # type: ignore[arg-type]
end_ts: datetime.datetime | None = TimestampConverter.from_unix_millis(
data["EndTimestamp"]
) # type: ignore[arg-type]

if start_ts is None or end_ts is None:
raise InvalidParameterValueException(
"StartTimestamp and EndTimestamp cannot be null"
)

return cls(
start_timestamp=start_ts,
end_timestamp=end_ts,
request_id=data["RequestId"],
)

def to_dict(self) -> dict[str, Any]:
return {
"StartTimestamp": self.start_timestamp,
"EndTimestamp": self.end_timestamp,
"RequestId": self.request_id,
}

def to_json_dict(self) -> dict[str, Any]:
"""Convert to JSON-serializable dict with Unix millisecond timestamps."""
return {
"StartTimestamp": TimestampConverter.to_unix_millis(self.start_timestamp),
"EndTimestamp": TimestampConverter.to_unix_millis(self.end_timestamp),
"RequestId": self.request_id,
}


# endregion event_structures

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,16 +291,22 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: #
Returns:
HTTPResponse: The HTTP response to send to the client
"""
logger.debug("🌟 HANDLER: Received POST /start-durable-execution request")
try:
body_data: dict[str, Any] = self._parse_json_body(request)
logger.debug("🌟 HANDLER: Parsed request body successfully")

start_input: StartDurableExecutionInput = (
StartDurableExecutionInput.from_dict(body_data)
)
logger.debug(
"🌟 HANDLER: Created StartDurableExecutionInput, calling executor.start_execution()"
)

start_output: StartDurableExecutionOutput = self.executor.start_execution(
start_input
)
logger.debug("🌟 HANDLER: executor.start_execution() returned successfully")

response_data: dict[str, Any] = start_output.to_dict()

Expand Down
103 changes: 102 additions & 1 deletion tests/model_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
from __future__ import annotations

import datetime
import json

import pytest

from aws_durable_execution_sdk_python.lambda_service import (
OperationStatus,
OperationType,
)

from aws_durable_execution_sdk_python_testing.exceptions import (
InvalidParameterValueException,
)
Expand Down Expand Up @@ -46,6 +47,7 @@
GetDurableExecutionResponse,
GetDurableExecutionStateRequest,
GetDurableExecutionStateResponse,
InvocationCompletedDetails,
ListDurableExecutionsByFunctionRequest,
ListDurableExecutionsByFunctionResponse,
ListDurableExecutionsRequest,
Expand Down Expand Up @@ -3564,3 +3566,102 @@ def test_events_to_operations_invalid_sub_type():
match=f"'{invalid_sub_type}' is not a valid OperationSubType",
):
events_to_operations([event])


def test_invocation_completed_details_to_json_dict():
"""Test InvocationCompletedDetails.to_json_dict() converts datetime to Unix milliseconds."""
start_time = datetime.datetime(2023, 1, 1, 0, 0, 0, 123456, tzinfo=datetime.UTC)
end_time = datetime.datetime(2023, 1, 1, 0, 1, 0, 456789, tzinfo=datetime.UTC)

details = InvocationCompletedDetails(
start_timestamp=start_time, end_timestamp=end_time, request_id="req-123"
)

json_dict = details.to_json_dict()

# Verify timestamps are converted to Unix milliseconds (integers)
assert json_dict["StartTimestamp"] == 1672531200123
assert json_dict["EndTimestamp"] == 1672531260456
assert json_dict["RequestId"] == "req-123"

# Verify all values are JSON-serializable
json_str = json.dumps(json_dict)
assert json_str is not None


def test_invocation_completed_details_from_json_dict():
"""Test InvocationCompletedDetails.from_json_dict() converts Unix milliseconds to datetime."""
json_dict = {
"StartTimestamp": 1672531200123,
"EndTimestamp": 1672531260456,
"RequestId": "req-456",
}

details = InvocationCompletedDetails.from_json_dict(json_dict)

# Verify timestamps are converted to datetime objects
assert details.start_timestamp == datetime.datetime(
2023, 1, 1, 0, 0, 0, 123000, tzinfo=datetime.UTC
)
assert details.end_timestamp == datetime.datetime(
2023, 1, 1, 0, 1, 0, 456000, tzinfo=datetime.UTC
)
assert details.request_id == "req-456"


def test_invocation_completed_details_json_round_trip():
"""Test InvocationCompletedDetails to_json_dict/from_json_dict round-trip."""
original = InvocationCompletedDetails(
start_timestamp=datetime.datetime(
2023, 6, 15, 12, 30, 45, 678000, tzinfo=datetime.UTC
),
end_timestamp=datetime.datetime(
2023, 6, 15, 12, 31, 50, 123000, tzinfo=datetime.UTC
),
request_id="round-trip-test",
)

# Serialize to JSON dict
json_dict = original.to_json_dict()

# Deserialize back
restored = InvocationCompletedDetails.from_json_dict(json_dict)

# Verify round-trip preserves data
assert restored.start_timestamp == original.start_timestamp
assert restored.end_timestamp == original.end_timestamp
assert restored.request_id == original.request_id


def test_invocation_completed_details_to_dict_preserves_datetime():
"""Test InvocationCompletedDetails.to_dict() preserves datetime objects (not converted)."""
start_time = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
end_time = datetime.datetime(2023, 1, 1, 0, 1, 0, tzinfo=datetime.UTC)

details = InvocationCompletedDetails(
start_timestamp=start_time, end_timestamp=end_time, request_id="req-789"
)

regular_dict = details.to_dict()

# Verify to_dict() preserves datetime objects (not converted to Unix milliseconds)
assert regular_dict["StartTimestamp"] == start_time
assert regular_dict["EndTimestamp"] == end_time
assert isinstance(regular_dict["StartTimestamp"], datetime.datetime)
assert isinstance(regular_dict["EndTimestamp"], datetime.datetime)


def test_invocation_completed_details_from_json_dict_invalid_timestamp():
"""Test InvocationCompletedDetails.from_json_dict() raises error for invalid timestamps."""
# Test with invalid timestamp that would return None
json_dict = {
"StartTimestamp": None,
"EndTimestamp": 1672531260456,
"RequestId": "req-error",
}

with pytest.raises(
InvalidParameterValueException,
match="StartTimestamp and EndTimestamp cannot be null",
):
InvocationCompletedDetails.from_json_dict(json_dict)
Loading