Skip to content

Commit 0fdc0e2

Browse files
author
Rares Polenciuc
committed
refactor: replace dataclasses.replace with explicit factory methods
- Add Operation.from_existing() classmethod - Add StepDetails.from_existing() and CallbackDetails.from_existing() factory methods - Replace all dataclasses.replace() calls with explicit constructors - Add test coverage for new factory methods
1 parent 245dd3c commit 0fdc0e2

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed

src/aws_durable_execution_sdk_python/lambda_service.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ def from_dict(cls, data: MutableMapping[str, Any]) -> StepDetails:
187187
error=ErrorObject.from_dict(error_raw) if error_raw else None,
188188
)
189189

190+
@classmethod
191+
def from_existing(
192+
cls,
193+
step_details: StepDetails,
194+
next_attempt_timestamp: datetime.datetime | None = None,
195+
) -> StepDetails:
196+
"""Create new StepDetails from existing with cleared next_attempt_timestamp."""
197+
return cls(
198+
attempt=step_details.attempt,
199+
next_attempt_timestamp=next_attempt_timestamp,
200+
result=step_details.result,
201+
error=step_details.error,
202+
)
203+
190204

191205
@dataclass(frozen=True)
192206
class WaitDetails:
@@ -212,6 +226,20 @@ def from_dict(cls, data: MutableMapping[str, Any]) -> CallbackDetails:
212226
error=ErrorObject.from_dict(error_raw) if error_raw else None,
213227
)
214228

229+
@classmethod
230+
def from_existing(
231+
cls,
232+
callback_details: CallbackDetails,
233+
result: str | None = None,
234+
error: ErrorObject | None = None,
235+
) -> CallbackDetails:
236+
"""Create new CallbackDetails from existing with updated result or error."""
237+
return cls(
238+
callback_id=callback_details.callback_id,
239+
result=callback_details.result if result is None else result,
240+
error=callback_details.error if error is None else error,
241+
)
242+
215243

216244
@dataclass(frozen=True)
217245
class ChainedInvokeDetails:
@@ -708,6 +736,57 @@ class Operation:
708736
callback_details: CallbackDetails | None = None
709737
chained_invoke_details: ChainedInvokeDetails | None = None
710738

739+
@classmethod
740+
def from_existing(
741+
cls,
742+
operation: Operation,
743+
status: OperationStatus | None = None,
744+
name: str | None = None,
745+
parent_id: str | None = None,
746+
sub_type: OperationSubType | None = None,
747+
start_timestamp: datetime.datetime | None = None,
748+
end_timestamp: datetime.datetime | None = None,
749+
execution_details: ExecutionDetails | None = None,
750+
context_details: ContextDetails | None = None,
751+
step_details: StepDetails | None = None,
752+
wait_details: WaitDetails | None = None,
753+
callback_details: CallbackDetails | None = None,
754+
chained_invoke_details: ChainedInvokeDetails | None = None,
755+
) -> Operation:
756+
"""Create a new Operation by copying an existing one with selective field updates."""
757+
return cls(
758+
operation_id=operation.operation_id,
759+
operation_type=operation.operation_type,
760+
status=operation.status if status is None else status,
761+
parent_id=operation.parent_id if parent_id is None else parent_id,
762+
name=operation.name if name is None else name,
763+
start_timestamp=operation.start_timestamp
764+
if start_timestamp is None
765+
else start_timestamp,
766+
end_timestamp=operation.end_timestamp
767+
if end_timestamp is None
768+
else end_timestamp,
769+
sub_type=operation.sub_type if sub_type is None else sub_type,
770+
execution_details=operation.execution_details
771+
if execution_details is None
772+
else execution_details,
773+
context_details=operation.context_details
774+
if context_details is None
775+
else context_details,
776+
step_details=operation.step_details
777+
if step_details is None
778+
else step_details,
779+
wait_details=operation.wait_details
780+
if wait_details is None
781+
else wait_details,
782+
callback_details=operation.callback_details
783+
if callback_details is None
784+
else callback_details,
785+
chained_invoke_details=operation.chained_invoke_details
786+
if chained_invoke_details is None
787+
else chained_invoke_details,
788+
)
789+
711790
@classmethod
712791
def from_dict(cls, data: MutableMapping[str, Any]) -> Operation:
713792
"""Create an Operation instance from a dictionary with the original Smithy model field names.

tests/lambda_service_test.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,23 @@ def test_step_details_minimal():
277277
assert details.error is None
278278

279279

280+
def test_step_details_from_existing():
281+
"""Test StepDetails.from_existing factory method."""
282+
original = StepDetails(
283+
attempt=2,
284+
next_attempt_timestamp=datetime.datetime(2023, 1, 1, tzinfo=datetime.UTC),
285+
result="original_result",
286+
error=ErrorObject.from_message("original_error"),
287+
)
288+
289+
# Test clearing next_attempt_timestamp
290+
updated = StepDetails.from_existing(original, next_attempt_timestamp=None)
291+
assert updated.attempt == 2
292+
assert updated.next_attempt_timestamp is None
293+
assert updated.result == "original_result"
294+
assert updated.error.message == "original_error"
295+
296+
280297
def test_wait_details_from_dict():
281298
"""Test WaitDetails.from_dict method."""
282299
timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.UTC)
@@ -330,6 +347,36 @@ def test_callback_details_minimal():
330347
assert details.error is None
331348

332349

350+
def test_callback_details_from_existing_with_result():
351+
"""Test CallbackDetails.from_existing factory method with result update."""
352+
original = CallbackDetails(
353+
callback_id="cb123",
354+
result="original_result",
355+
error=ErrorObject.from_message("original_error"),
356+
)
357+
358+
# Test updating result
359+
updated = CallbackDetails.from_existing(original, result="new_result")
360+
assert updated.callback_id == "cb123"
361+
assert updated.result == "new_result"
362+
assert updated.error.message == "original_error"
363+
364+
365+
def test_callback_details_from_existing_with_error():
366+
"""Test CallbackDetails.from_existing factory method with error update."""
367+
original = CallbackDetails(
368+
callback_id="cb123",
369+
result="original_result",
370+
error=None,
371+
)
372+
373+
new_error = ErrorObject.from_message("new_error")
374+
updated = CallbackDetails.from_existing(original, error=new_error)
375+
assert updated.callback_id == "cb123"
376+
assert updated.result == "original_result"
377+
assert updated.error.message == "new_error"
378+
379+
333380
def test_invoke_details_from_dict():
334381
"""Test ChainedInvokeDetails.from_dict method."""
335382
error_data = {"ErrorMessage": "Invoke error"}
@@ -1119,6 +1166,27 @@ def test_operation_from_dict_no_options():
11191166
assert operation.operation_id == "test-id"
11201167

11211168

1169+
def test_operation_from_existing_with_callback_details():
1170+
"""Test Operation.from_existing factory method with callback_details update."""
1171+
original = Operation(
1172+
operation_id="op1",
1173+
operation_type=OperationType.CALLBACK,
1174+
status=OperationStatus.STARTED,
1175+
callback_details=CallbackDetails(callback_id="cb123", result=None),
1176+
)
1177+
1178+
new_callback_details = CallbackDetails(callback_id="cb123", result="success_result")
1179+
updated = Operation.from_existing(
1180+
original,
1181+
status=OperationStatus.SUCCEEDED,
1182+
callback_details=new_callback_details,
1183+
)
1184+
1185+
assert updated.status == OperationStatus.SUCCEEDED
1186+
assert updated.callback_details.callback_id == "cb123"
1187+
assert updated.callback_details.result == "success_result"
1188+
1189+
11221190
def test_operation_from_dict_individual_options():
11231191
"""Test Operation.from_dict with each option type individually."""
11241192
# Test with just ContextOptions
@@ -1510,6 +1578,60 @@ def test_operation_to_dict_all_optional_fields():
15101578
assert result["SubType"] == "Step"
15111579

15121580

1581+
def test_operation_from_existing():
1582+
"""Test Operation.from_existing factory method."""
1583+
original = Operation(
1584+
operation_id="op1",
1585+
operation_type=OperationType.STEP,
1586+
status=OperationStatus.STARTED,
1587+
parent_id="parent1",
1588+
name="original_step",
1589+
start_timestamp=datetime.datetime(2023, 1, 1, tzinfo=datetime.UTC),
1590+
step_details=StepDetails(attempt=1, result="original_result"),
1591+
)
1592+
1593+
# Test updating status and end_timestamp
1594+
updated = Operation.from_existing(
1595+
original,
1596+
status=OperationStatus.SUCCEEDED,
1597+
end_timestamp=datetime.datetime(2023, 1, 2, tzinfo=datetime.UTC),
1598+
)
1599+
1600+
assert updated.operation_id == "op1"
1601+
assert updated.operation_type == OperationType.STEP
1602+
assert updated.status == OperationStatus.SUCCEEDED
1603+
assert updated.parent_id == "parent1"
1604+
assert updated.name == "original_step"
1605+
assert updated.start_timestamp == datetime.datetime(2023, 1, 1, tzinfo=datetime.UTC)
1606+
assert updated.end_timestamp == datetime.datetime(2023, 1, 2, tzinfo=datetime.UTC)
1607+
assert updated.step_details.attempt == 1
1608+
assert updated.step_details.result == "original_result"
1609+
1610+
1611+
def test_operation_from_existing_with_step_details():
1612+
"""Test Operation.from_existing factory method with step_details update."""
1613+
original = Operation(
1614+
operation_id="op1",
1615+
operation_type=OperationType.STEP,
1616+
status=OperationStatus.PENDING,
1617+
step_details=StepDetails(
1618+
attempt=1,
1619+
next_attempt_timestamp=datetime.datetime(2023, 1, 1, tzinfo=datetime.UTC),
1620+
),
1621+
)
1622+
1623+
new_step_details = StepDetails(attempt=1, next_attempt_timestamp=None)
1624+
updated = Operation.from_existing(
1625+
original,
1626+
status=OperationStatus.READY,
1627+
step_details=new_step_details,
1628+
)
1629+
1630+
assert updated.status == OperationStatus.READY
1631+
assert updated.step_details.attempt == 1
1632+
assert updated.step_details.next_attempt_timestamp is None
1633+
1634+
15131635
# =============================================================================
15141636
# Tests for Checkpoint Classes
15151637
# =============================================================================

0 commit comments

Comments
 (0)