Coverage for async_durable_execution/_runner/model.py: 99%

706 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-08-30 23:43 +0000

1"""Shared runner models.""" 

2 

3from __future__ import annotations 

4 

5import datetime 

6import json 

7import logging 

8from dataclasses import dataclass, field, replace 

9from enum import Enum 

10from typing import Any, Protocol, TYPE_CHECKING 

11 

12from .._core import ( 

13 AwsApiModel, 

14 CallbackDetails, 

15 CallbackOptions, 

16 ChainedInvokeDetails, 

17 ContextDetails, 

18 DurableExecutionInvocationOutput, 

19 ErrorObject, 

20 ExecutionDetails, 

21 ExtendedTypeSerDes, 

22 InvocationStatus, 

23 Operation, 

24 OperationAction, 

25 OperationPayload, 

26 OperationStatus, 

27 OperationSubType, 

28 OperationType, 

29 OperationUpdate, 

30 StepDetails, 

31 WaitDetails, 

32) 

33from .exceptions import ( 

34 DurableFunctionsTestError, 

35 InvalidParameterValueException, 

36) 

37 

38if TYPE_CHECKING: 

39 from .local.model import StartDurableExecutionInput 

40 

41 

42logger = logging.getLogger(__name__) 

43 

44 

45class EventType(Enum): 

46 """Event types for durable execution events.""" 

47 

48 EXECUTION_STARTED = "ExecutionStarted" 

49 EXECUTION_SUCCEEDED = "ExecutionSucceeded" 

50 EXECUTION_FAILED = "ExecutionFailed" 

51 EXECUTION_TIMED_OUT = "ExecutionTimedOut" 

52 EXECUTION_STOPPED = "ExecutionStopped" 

53 CONTEXT_STARTED = "ContextStarted" 

54 CONTEXT_SUCCEEDED = "ContextSucceeded" 

55 CONTEXT_FAILED = "ContextFailed" 

56 WAIT_STARTED = "WaitStarted" 

57 WAIT_SUCCEEDED = "WaitSucceeded" 

58 WAIT_CANCELLED = "WaitCancelled" 

59 STEP_STARTED = "StepStarted" 

60 STEP_SUCCEEDED = "StepSucceeded" 

61 STEP_FAILED = "StepFailed" 

62 CHAINED_INVOKE_STARTED = "ChainedInvokeStarted" 

63 CHAINED_INVOKE_SUCCEEDED = "ChainedInvokeSucceeded" 

64 CHAINED_INVOKE_FAILED = "ChainedInvokeFailed" 

65 CHAINED_INVOKE_TIMED_OUT = "ChainedInvokeTimedOut" 

66 CHAINED_INVOKE_STOPPED = "ChainedInvokeStopped" 

67 CALLBACK_STARTED = "CallbackStarted" 

68 CALLBACK_SUCCEEDED = "CallbackSucceeded" 

69 CALLBACK_FAILED = "CallbackFailed" 

70 CALLBACK_TIMED_OUT = "CallbackTimedOut" 

71 INVOCATION_COMPLETED = "InvocationCompleted" 

72 

73 

74TERMINAL_STATUSES: set[OperationStatus] = { 

75 OperationStatus.SUCCEEDED, 

76 OperationStatus.FAILED, 

77 OperationStatus.TIMED_OUT, 

78 OperationStatus.STOPPED, 

79 OperationStatus.CANCELLED, 

80} 

81 

82 

83@dataclass(frozen=True) 

84class GetDurableExecutionResponse(AwsApiModel): 

85 """Response containing durable execution details.""" 

86 

87 durable_execution_arn: str = field(metadata={"alias": "DurableExecutionArn"}) 

88 durable_execution_name: str = field(metadata={"alias": "DurableExecutionName"}) 

89 function_arn: str = field(metadata={"alias": "FunctionArn"}) 

90 status: str = field(metadata={"alias": "Status"}) 

91 start_timestamp: datetime.datetime = field( 

92 metadata={"alias": "StartTimestamp", "is_timestamp": True} 

93 ) 

94 input_payload: str | None = field(default=None, metadata={"alias": "InputPayload"}) 

95 result: str | None = field(default=None, metadata={"alias": "Result"}) 

96 error: ErrorObject | None = field(default=None, metadata={"alias": "Error"}) 

97 end_timestamp: datetime.datetime | None = field( 

98 default=None, metadata={"alias": "EndTimestamp", "is_timestamp": True} 

99 ) 

100 version: str | None = field(default=None, metadata={"alias": "Version"}) 

101 

102 

103# Event-related structures from Smithy model 

104@dataclass(frozen=True) 

105class EventInput(AwsApiModel): 

106 """Event input structure.""" 

107 

108 payload: str | None = field(default=None, metadata={"alias": "Payload"}) 

109 truncated: bool = field(default=False, metadata={"alias": "Truncated"}) 

110 

111 @classmethod 

112 def from_details( 

113 cls, 

114 details: ExecutionDetails, 

115 include: bool = False, # noqa: FBT001, FBT002 

116 ) -> EventInput: 

117 details_input: str | None = details.input_payload if details else None 

118 payload: str | None = details_input if include else None 

119 truncated: bool = not include 

120 return cls(payload=payload, truncated=truncated) 

121 

122 @classmethod 

123 def from_start_durable_execution_input( 

124 cls, 

125 start_durable_execution_input: StartDurableExecutionInput, 

126 include: bool = False, # noqa: FBT001, FBT002 

127 ) -> EventInput: 

128 input: str | None = start_durable_execution_input.input 

129 truncated: bool = not include 

130 return cls(input, truncated) 

131 

132 

133@dataclass(frozen=True) 

134class EventResult(AwsApiModel): 

135 """Event result structure.""" 

136 

137 payload: str | None = field(default=None, metadata={"alias": "Payload"}) 

138 truncated: bool = field(default=False, metadata={"alias": "Truncated"}) 

139 

140 @classmethod 

141 def from_details( 

142 cls, 

143 details: CallbackDetails | StepDetails | ChainedInvokeDetails | ContextDetails, 

144 include: bool = False, # noqa: FBT001, FBT002 

145 ) -> EventResult: 

146 details_result: str | None = details.result if details else None 

147 payload: str | None = details_result if include else None 

148 truncated: bool = not include 

149 return cls(payload=payload, truncated=truncated) 

150 

151 @classmethod 

152 def from_durable_execution_invocation_output( 

153 cls, 

154 durable_execution_invocation_output: DurableExecutionInvocationOutput, 

155 include: bool = False, # noqa: FBT001, FBT002 

156 ) -> EventResult: 

157 truncated: bool = not include 

158 return cls(durable_execution_invocation_output.result, truncated) 

159 

160 

161@dataclass(frozen=True) 

162class EventError(AwsApiModel): 

163 """Event error structure.""" 

164 

165 payload: ErrorObject | None = field(default=None, metadata={"alias": "Payload"}) 

166 truncated: bool = field(default=False, metadata={"alias": "Truncated"}) 

167 

168 @classmethod 

169 def from_details( 

170 cls, 

171 details: CallbackDetails | StepDetails | ChainedInvokeDetails | ContextDetails, 

172 include: bool = False, # noqa: FBT001, FBT002 

173 ) -> EventError: 

174 error_object: ErrorObject | None = details.error if details else None 

175 truncated: bool = not include 

176 return cls(error_object, truncated) 

177 

178 @classmethod 

179 def from_durable_execution_invocation_output( 

180 cls, 

181 durable_execution_invocation_output: DurableExecutionInvocationOutput, 

182 include: bool = False, # noqa: FBT001, FBT002 

183 ) -> EventError: 

184 truncated: bool = not include 

185 return cls(durable_execution_invocation_output.error, truncated) 

186 

187 

188@dataclass(frozen=True) 

189class RetryDetails(AwsApiModel): 

190 """Retry details structure.""" 

191 

192 current_attempt: int = field(default=0, metadata={"alias": "CurrentAttempt"}) 

193 next_attempt_delay_seconds: int | None = field( 

194 default=None, metadata={"alias": "NextAttemptDelaySeconds"} 

195 ) 

196 

197 

198# Event detail structures 

199@dataclass(frozen=True) 

200class ExecutionStartedDetails(AwsApiModel): 

201 """Execution started event details.""" 

202 

203 input: EventInput | None = field(default=None, metadata={"alias": "Input"}) 

204 execution_timeout: int | None = field( 

205 default=None, metadata={"alias": "ExecutionTimeout"} 

206 ) 

207 

208 

209@dataclass(frozen=True) 

210class ExecutionSucceededDetails(AwsApiModel): 

211 """Execution succeeded event details.""" 

212 

213 result: EventResult | None = field(default=None, metadata={"alias": "Result"}) 

214 

215 

216@dataclass(frozen=True) 

217class ExecutionFailedDetails(AwsApiModel): 

218 """Execution failed event details.""" 

219 

220 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

221 

222 

223@dataclass(frozen=True) 

224class ExecutionTimedOutDetails(AwsApiModel): 

225 """Execution timed out event details.""" 

226 

227 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

228 

229 

230@dataclass(frozen=True) 

231class ExecutionStoppedDetails(AwsApiModel): 

232 """Execution stopped event details.""" 

233 

234 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

235 

236 

237@dataclass(frozen=True) 

238class ContextStartedDetails(AwsApiModel): 

239 """Context started event details.""" 

240 

241 

242@dataclass(frozen=True) 

243class ContextSucceededDetails(AwsApiModel): 

244 """Context succeeded event details.""" 

245 

246 result: EventResult | None = field(default=None, metadata={"alias": "Result"}) 

247 

248 

249@dataclass(frozen=True) 

250class ContextFailedDetails(AwsApiModel): 

251 """Context failed event details.""" 

252 

253 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

254 

255 

256@dataclass(frozen=True) 

257class WaitStartedDetails(AwsApiModel): 

258 """Wait started event details.""" 

259 

260 duration: int | None = field(default=None, metadata={"alias": "Duration"}) 

261 scheduled_end_timestamp: datetime.datetime | None = field( 

262 default=None, 

263 metadata={"alias": "ScheduledEndTimestamp", "is_timestamp": True}, 

264 ) 

265 

266 

267@dataclass(frozen=True) 

268class WaitSucceededDetails(AwsApiModel): 

269 """Wait succeeded event details.""" 

270 

271 duration: int | None = field(default=None, metadata={"alias": "Duration"}) 

272 

273 

274@dataclass(frozen=True) 

275class WaitCancelledDetails(AwsApiModel): 

276 """Wait cancelled event details.""" 

277 

278 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

279 

280 

281@dataclass(frozen=True) 

282class StepStartedDetails(AwsApiModel): 

283 """Step started event details.""" 

284 

285 

286@dataclass(frozen=True) 

287class StepSucceededDetails(AwsApiModel): 

288 """Step succeeded event details.""" 

289 

290 result: EventResult | None = field(default=None, metadata={"alias": "Result"}) 

291 retry_details: RetryDetails | None = field( 

292 default=None, metadata={"alias": "RetryDetails"} 

293 ) 

294 

295 

296@dataclass(frozen=True) 

297class StepFailedDetails(AwsApiModel): 

298 """Step failed event details.""" 

299 

300 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

301 retry_details: RetryDetails | None = field( 

302 default=None, metadata={"alias": "RetryDetails"} 

303 ) 

304 

305 

306@dataclass(frozen=True) 

307class ChainedInvokePendingDetails(AwsApiModel): 

308 """Chained Invoke Pending event details.""" 

309 

310 input: EventInput | None = field(default=None, metadata={"alias": "Input"}) 

311 function_name: str | None = field(default=None, metadata={"alias": "FunctionName"}) 

312 

313 

314@dataclass(frozen=True) 

315class ChainedInvokeStartedDetails(AwsApiModel): 

316 """Chained invoke started event details.""" 

317 

318 durable_execution_arn: str | None = field( 

319 default=None, metadata={"alias": "DurableExecutionArn"} 

320 ) 

321 

322 

323@dataclass(frozen=True) 

324class ChainedInvokeSucceededDetails(AwsApiModel): 

325 """Chained invoke succeeded event details.""" 

326 

327 result: EventResult | None = field(default=None, metadata={"alias": "Result"}) 

328 

329 

330@dataclass(frozen=True) 

331class ChainedInvokeFailedDetails(AwsApiModel): 

332 """Chained invoke failed event details.""" 

333 

334 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

335 

336 

337@dataclass(frozen=True) 

338class ChainedInvokeTimedOutDetails(AwsApiModel): 

339 """Chained invoke timed out event details.""" 

340 

341 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

342 

343 

344@dataclass(frozen=True) 

345class ChainedInvokeStoppedDetails(AwsApiModel): 

346 """Chained invoke stopped event details.""" 

347 

348 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

349 

350 

351@dataclass(frozen=True) 

352class CallbackStartedDetails(AwsApiModel): 

353 """Callback started event details.""" 

354 

355 callback_id: str | None = field(default=None, metadata={"alias": "CallbackId"}) 

356 heartbeat_timeout: int | None = field( 

357 default=None, metadata={"alias": "HeartbeatTimeout"} 

358 ) 

359 timeout: int | None = field(default=None, metadata={"alias": "Timeout"}) 

360 

361 

362@dataclass(frozen=True) 

363class CallbackSucceededDetails(AwsApiModel): 

364 """Callback succeeded event details.""" 

365 

366 result: EventResult | None = field(default=None, metadata={"alias": "Result"}) 

367 

368 

369@dataclass(frozen=True) 

370class CallbackFailedDetails(AwsApiModel): 

371 """Callback failed event details.""" 

372 

373 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

374 

375 

376@dataclass(frozen=True) 

377class CallbackTimedOutDetails(AwsApiModel): 

378 """Callback timed out event details.""" 

379 

380 error: EventError | None = field(default=None, metadata={"alias": "Error"}) 

381 

382 

383@dataclass(frozen=True) 

384class InvocationCompletedDetails(AwsApiModel): 

385 """Invocation completed event details.""" 

386 

387 start_timestamp: datetime.datetime = field( 

388 metadata={"alias": "StartTimestamp", "is_timestamp": True} 

389 ) 

390 end_timestamp: datetime.datetime = field( 

391 metadata={"alias": "EndTimestamp", "is_timestamp": True} 

392 ) 

393 request_id: str = field(metadata={"alias": "RequestId"}) 

394 

395 

396@dataclass(frozen=True) 

397class EventCreationContext: 

398 operation: Operation 

399 event_id: int 

400 durable_execution_arn: str 

401 start_durable_execution_input: StartDurableExecutionInput 

402 durable_execution_invocation_output: DurableExecutionInvocationOutput | None = None 

403 operation_update: OperationUpdate | None = None 

404 include_execution_data: bool = False 

405 

406 @property 

407 def sub_type(self) -> str | None: 

408 sub_type = self.operation.sub_type 

409 if isinstance(sub_type, OperationSubType): 

410 return sub_type.value 

411 return sub_type 

412 

413 def get_retry_details(self) -> RetryDetails | None: 

414 if not self.operation.step_details or not self.operation_update: 

415 return None 

416 

417 delay = 0 

418 if ( 

419 self.operation_update.operation_type == OperationType.STEP 

420 and self.operation_update.step_options 

421 ): 

422 delay = self.operation_update.step_options.next_attempt_delay_seconds 

423 

424 return RetryDetails( 

425 current_attempt=self.operation.step_details.attempt, 

426 next_attempt_delay_seconds=delay, 

427 ) 

428 

429 @property 

430 def start_timestamp(self) -> datetime.datetime: 

431 return ( 

432 self.operation.start_timestamp 

433 if self.operation.start_timestamp is not None 

434 else datetime.datetime.now(datetime.timezone.utc) 

435 ) 

436 

437 @property 

438 def end_timestamp(self) -> datetime.datetime: 

439 return ( 

440 self.operation.end_timestamp 

441 if self.operation.end_timestamp is not None 

442 else datetime.datetime.now(datetime.timezone.utc) 

443 ) 

444 

445 

446@dataclass(frozen=True) 

447class Event(AwsApiModel): 

448 """Event structure from Smithy model.""" 

449 

450 event_type: str = field(metadata={"alias": "EventType"}) 

451 event_timestamp: datetime.datetime = field( 

452 metadata={"alias": "EventTimestamp", "is_timestamp": True} 

453 ) 

454 sub_type: str | None = field(default=None, metadata={"alias": "SubType"}) 

455 event_id: int = field(default=1, metadata={"alias": "EventId"}) 

456 operation_id: str | None = field(default=None, metadata={"alias": "Id"}) 

457 name: str | None = field(default=None, metadata={"alias": "Name"}) 

458 parent_id: str | None = field(default=None, metadata={"alias": "ParentId"}) 

459 execution_started_details: ExecutionStartedDetails | None = field( 

460 default=None, metadata={"alias": "ExecutionStartedDetails"} 

461 ) 

462 execution_succeeded_details: ExecutionSucceededDetails | None = field( 

463 default=None, metadata={"alias": "ExecutionSucceededDetails"} 

464 ) 

465 execution_failed_details: ExecutionFailedDetails | None = field( 

466 default=None, metadata={"alias": "ExecutionFailedDetails"} 

467 ) 

468 execution_timed_out_details: ExecutionTimedOutDetails | None = field( 

469 default=None, metadata={"alias": "ExecutionTimedOutDetails"} 

470 ) 

471 execution_stopped_details: ExecutionStoppedDetails | None = field( 

472 default=None, metadata={"alias": "ExecutionStoppedDetails"} 

473 ) 

474 context_started_details: ContextStartedDetails | None = field( 

475 default=None, metadata={"alias": "ContextStartedDetails"} 

476 ) 

477 context_succeeded_details: ContextSucceededDetails | None = field( 

478 default=None, metadata={"alias": "ContextSucceededDetails"} 

479 ) 

480 context_failed_details: ContextFailedDetails | None = field( 

481 default=None, metadata={"alias": "ContextFailedDetails"} 

482 ) 

483 wait_started_details: WaitStartedDetails | None = field( 

484 default=None, metadata={"alias": "WaitStartedDetails"} 

485 ) 

486 wait_succeeded_details: WaitSucceededDetails | None = field( 

487 default=None, metadata={"alias": "WaitSucceededDetails"} 

488 ) 

489 wait_cancelled_details: WaitCancelledDetails | None = field( 

490 default=None, metadata={"alias": "WaitCancelledDetails"} 

491 ) 

492 step_started_details: StepStartedDetails | None = field( 

493 default=None, metadata={"alias": "StepStartedDetails"} 

494 ) 

495 step_succeeded_details: StepSucceededDetails | None = field( 

496 default=None, metadata={"alias": "StepSucceededDetails"} 

497 ) 

498 step_failed_details: StepFailedDetails | None = field( 

499 default=None, metadata={"alias": "StepFailedDetails"} 

500 ) 

501 chained_invoke_pending_details: ChainedInvokePendingDetails | None = field( 

502 default=None, metadata={"alias": "ChainedInvokePendingDetails"} 

503 ) 

504 chained_invoke_started_details: ChainedInvokeStartedDetails | None = field( 

505 default=None, metadata={"alias": "ChainedInvokeStartedDetails"} 

506 ) 

507 chained_invoke_succeeded_details: ChainedInvokeSucceededDetails | None = field( 

508 default=None, metadata={"alias": "ChainedInvokeSucceededDetails"} 

509 ) 

510 chained_invoke_failed_details: ChainedInvokeFailedDetails | None = field( 

511 default=None, metadata={"alias": "ChainedInvokeFailedDetails"} 

512 ) 

513 chained_invoke_timed_out_details: ChainedInvokeTimedOutDetails | None = field( 

514 default=None, metadata={"alias": "ChainedInvokeTimedOutDetails"} 

515 ) 

516 chained_invoke_stopped_details: ChainedInvokeStoppedDetails | None = field( 

517 default=None, metadata={"alias": "ChainedInvokeStoppedDetails"} 

518 ) 

519 callback_started_details: CallbackStartedDetails | None = field( 

520 default=None, metadata={"alias": "CallbackStartedDetails"} 

521 ) 

522 callback_succeeded_details: CallbackSucceededDetails | None = field( 

523 default=None, metadata={"alias": "CallbackSucceededDetails"} 

524 ) 

525 callback_failed_details: CallbackFailedDetails | None = field( 

526 default=None, metadata={"alias": "CallbackFailedDetails"} 

527 ) 

528 callback_timed_out_details: CallbackTimedOutDetails | None = field( 

529 default=None, metadata={"alias": "CallbackTimedOutDetails"} 

530 ) 

531 invocation_completed_details: InvocationCompletedDetails | None = field( 

532 default=None, metadata={"alias": "InvocationCompletedDetails"} 

533 ) 

534 

535 @classmethod 

536 def create_execution_event_started(cls, context: EventCreationContext) -> Event: 

537 execution_details: ExecutionDetails | None = context.operation.execution_details 

538 event_input: EventInput | None = ( 

539 EventInput.from_details(execution_details, context.include_execution_data) 

540 if execution_details 

541 else None 

542 ) 

543 execution_timeout: int | None = ( 

544 context.start_durable_execution_input.execution_timeout_seconds 

545 ) 

546 

547 return cls( 

548 event_type=EventType.EXECUTION_STARTED.value, 

549 event_timestamp=context.start_timestamp, 

550 sub_type=context.sub_type, 

551 event_id=context.event_id, 

552 operation_id=context.operation.operation_id, 

553 name=context.operation.name, 

554 parent_id=context.operation.parent_id, 

555 execution_started_details=ExecutionStartedDetails( 

556 input=event_input, 

557 execution_timeout=execution_timeout, 

558 ), 

559 ) 

560 

561 @classmethod 

562 def create_execution_event_succeeded(cls, context: EventCreationContext) -> Event: 

563 result: EventResult | None = ( 

564 EventResult.from_durable_execution_invocation_output( 

565 context.durable_execution_invocation_output, 

566 context.include_execution_data, 

567 ) 

568 if context.durable_execution_invocation_output 

569 else None 

570 ) 

571 return cls( 

572 event_type=EventType.EXECUTION_SUCCEEDED.value, 

573 event_timestamp=context.end_timestamp, 

574 sub_type=context.sub_type, 

575 event_id=context.event_id, 

576 operation_id=context.operation.operation_id, 

577 name=context.operation.name, 

578 parent_id=context.operation.parent_id, 

579 execution_succeeded_details=ExecutionSucceededDetails(result=result), 

580 ) 

581 

582 @classmethod 

583 def create_execution_event_failed(cls, context: EventCreationContext) -> Event: 

584 error: EventError | None = ( 

585 EventError.from_durable_execution_invocation_output( 

586 context.durable_execution_invocation_output, 

587 include=context.include_execution_data, 

588 ) 

589 if context.durable_execution_invocation_output 

590 else None 

591 ) 

592 return cls( 

593 event_type=EventType.EXECUTION_FAILED.value, 

594 event_timestamp=context.end_timestamp, 

595 sub_type=context.sub_type, 

596 event_id=context.event_id, 

597 operation_id=context.operation.operation_id, 

598 name=context.operation.name, 

599 parent_id=context.operation.parent_id, 

600 execution_failed_details=ExecutionFailedDetails(error=error), 

601 ) 

602 

603 @classmethod 

604 def create_execution_event_timed_out(cls, context: EventCreationContext) -> Event: 

605 error: EventError | None = ( 

606 EventError.from_durable_execution_invocation_output( 

607 context.durable_execution_invocation_output, 

608 include=context.include_execution_data, 

609 ) 

610 if context.durable_execution_invocation_output 

611 else None 

612 ) 

613 return cls( 

614 event_type=EventType.EXECUTION_TIMED_OUT.value, 

615 event_timestamp=context.end_timestamp, 

616 sub_type=context.sub_type, 

617 event_id=context.event_id, 

618 operation_id=context.operation.operation_id, 

619 name=context.operation.name, 

620 parent_id=context.operation.parent_id, 

621 execution_timed_out_details=ExecutionTimedOutDetails(error=error), 

622 ) 

623 

624 @classmethod 

625 def create_execution_event_stopped(cls, context: EventCreationContext) -> Event: 

626 error: EventError | None = ( 

627 EventError.from_durable_execution_invocation_output( 

628 context.durable_execution_invocation_output, 

629 include=context.include_execution_data, 

630 ) 

631 if context.durable_execution_invocation_output 

632 else None 

633 ) 

634 return cls( 

635 event_type=EventType.EXECUTION_STOPPED.value, 

636 event_timestamp=context.end_timestamp, 

637 sub_type=context.sub_type, 

638 event_id=context.event_id, 

639 operation_id=context.operation.operation_id, 

640 name=context.operation.name, 

641 parent_id=context.operation.parent_id, 

642 execution_stopped_details=ExecutionStoppedDetails(error=error), 

643 ) 

644 

645 @classmethod 

646 def create_execution_event(cls, context: EventCreationContext) -> Event: 

647 """Create execution event based on action.""" 

648 match context.operation.status: 

649 case OperationStatus.STARTED: 

650 return cls.create_execution_event_started(context) 

651 case OperationStatus.SUCCEEDED: 

652 return cls.create_execution_event_succeeded(context) 

653 case OperationStatus.FAILED: 

654 return cls.create_execution_event_failed(context) 

655 case OperationStatus.TIMED_OUT: 

656 return cls.create_execution_event_timed_out(context) 

657 case OperationStatus.STOPPED: 

658 return cls.create_execution_event_stopped(context) 

659 case _: 

660 msg = f"Operation status {context.operation.status} is not valid for execution operations. Valid statuses are: STARTED, SUCCEEDED, FAILED, TIMED_OUT, STOPPED" 

661 raise InvalidParameterValueException(msg) 

662 

663 @classmethod 

664 def create_context_event_started(cls, context: EventCreationContext) -> Event: 

665 return cls( 

666 event_type=EventType.CONTEXT_STARTED.value, 

667 event_timestamp=context.start_timestamp, 

668 sub_type=context.sub_type, 

669 event_id=context.event_id, 

670 operation_id=context.operation.operation_id, 

671 name=context.operation.name, 

672 parent_id=context.operation.parent_id, 

673 context_started_details=ContextStartedDetails(), 

674 ) 

675 

676 @classmethod 

677 def create_context_event_succeeded(cls, context: EventCreationContext) -> Event: 

678 context_details: ContextDetails | None = context.operation.context_details 

679 event_result: EventResult | None = ( 

680 EventResult.from_details(context_details, context.include_execution_data) 

681 if context_details 

682 else None 

683 ) 

684 return cls( 

685 event_type=EventType.CONTEXT_SUCCEEDED.value, 

686 event_timestamp=context.end_timestamp, 

687 sub_type=context.sub_type, 

688 event_id=context.event_id, 

689 operation_id=context.operation.operation_id, 

690 name=context.operation.name, 

691 parent_id=context.operation.parent_id, 

692 context_succeeded_details=ContextSucceededDetails(result=event_result), 

693 ) 

694 

695 @classmethod 

696 def create_context_event_failed(cls, context: EventCreationContext) -> Event: 

697 context_details: ContextDetails | None = context.operation.context_details 

698 event_error: EventError | None = ( 

699 EventError.from_details(context_details) if context_details else None 

700 ) 

701 return cls( 

702 event_type=EventType.CONTEXT_FAILED.value, 

703 event_timestamp=context.end_timestamp, 

704 sub_type=context.sub_type, 

705 event_id=context.event_id, 

706 operation_id=context.operation.operation_id, 

707 name=context.operation.name, 

708 parent_id=context.operation.parent_id, 

709 context_failed_details=ContextFailedDetails(error=event_error), 

710 ) 

711 

712 @classmethod 

713 def create_context_event(cls, context: EventCreationContext) -> Event: 

714 """Create context event based on action.""" 

715 match context.operation.status: 

716 case OperationStatus.STARTED: 

717 return cls.create_context_event_started(context) 

718 case OperationStatus.SUCCEEDED: 

719 return cls.create_context_event_succeeded(context) 

720 case OperationStatus.FAILED: 

721 return cls.create_context_event_failed(context) 

722 case _: 

723 msg = ( 

724 f"Operation status {context.operation.status} is not valid for context operations. " 

725 f"Valid statuses are: STARTED, SUCCEEDED, FAILED" 

726 ) 

727 raise InvalidParameterValueException(msg) 

728 

729 @classmethod 

730 def create_wait_event_started(cls, context: EventCreationContext) -> Event: 

731 wait_details: WaitDetails | None = context.operation.wait_details 

732 scheduled_end_timestamp: datetime.datetime | None = ( 

733 wait_details.scheduled_end_timestamp if wait_details else None 

734 ) 

735 duration: int | None = None 

736 if ( 736 ↛ 747line 736 didn't jump to line 747 because the condition on line 736 was always true

737 wait_details 

738 and wait_details.scheduled_end_timestamp 

739 and context.operation.start_timestamp 

740 ): 

741 duration = round( 

742 ( 

743 wait_details.scheduled_end_timestamp 

744 - context.operation.start_timestamp 

745 ).total_seconds() 

746 ) 

747 return cls( 

748 event_type=EventType.WAIT_STARTED.value, 

749 event_timestamp=context.start_timestamp, 

750 sub_type=context.sub_type, 

751 event_id=context.event_id, 

752 operation_id=context.operation.operation_id, 

753 name=context.operation.name, 

754 parent_id=context.operation.parent_id, 

755 wait_started_details=WaitStartedDetails( 

756 duration=duration, 

757 scheduled_end_timestamp=scheduled_end_timestamp, 

758 ), 

759 ) 

760 

761 @classmethod 

762 def create_wait_event_succeeded(cls, context: EventCreationContext) -> Event: 

763 wait_details: WaitDetails | None = context.operation.wait_details 

764 duration: int | None = None 

765 if ( 765 ↛ 775line 765 didn't jump to line 775 because the condition on line 765 was always true

766 wait_details 

767 and wait_details.scheduled_end_timestamp 

768 and context.operation.start_timestamp 

769 ): 

770 duration = round( 

771 ( 

772 wait_details.scheduled_end_timestamp - context.start_timestamp 

773 ).total_seconds() 

774 ) 

775 return cls( 

776 event_type=EventType.WAIT_SUCCEEDED.value, 

777 event_timestamp=context.end_timestamp, 

778 sub_type=context.sub_type, 

779 event_id=context.event_id, 

780 operation_id=context.operation.operation_id, 

781 name=context.operation.name, 

782 parent_id=context.operation.parent_id, 

783 wait_succeeded_details=WaitSucceededDetails(duration=duration), 

784 ) 

785 

786 @classmethod 

787 def create_wait_event_cancelled(cls, context: EventCreationContext) -> Event: 

788 error: EventError | None = None 

789 if ( 789 ↛ 794line 789 didn't jump to line 794 because the condition on line 789 was never true

790 context.operation_update 

791 and context.operation_update.operation_type == OperationType.WAIT 

792 and context.operation_update.action == OperationAction.CANCEL 

793 ): 

794 error = EventError( 

795 context.operation_update.error, not context.include_execution_data 

796 ) 

797 return cls( 

798 event_type=EventType.WAIT_CANCELLED.value, 

799 event_timestamp=context.end_timestamp, 

800 sub_type=context.sub_type, 

801 event_id=context.event_id, 

802 operation_id=context.operation.operation_id, 

803 name=context.operation.name, 

804 parent_id=context.operation.parent_id, 

805 wait_cancelled_details=WaitCancelledDetails(error=error), 

806 ) 

807 

808 @classmethod 

809 def create_wait_event(cls, context: EventCreationContext) -> Event: 

810 """Create wait event based on action.""" 

811 match context.operation.status: 

812 case OperationStatus.STARTED: 

813 return cls.create_wait_event_started(context) 

814 case OperationStatus.SUCCEEDED: 

815 return cls.create_wait_event_succeeded(context) 

816 case OperationStatus.CANCELLED: 

817 return cls.create_wait_event_cancelled(context) 

818 case _: 

819 msg = ( 

820 f"Operation status {context.operation.status} is not valid for wait operations. " 

821 f"Valid statuses are: STARTED, SUCCEEDED, CANCELLED" 

822 ) 

823 raise InvalidParameterValueException(msg) 

824 

825 @classmethod 

826 def create_step_event_started(cls, context: EventCreationContext) -> Event: 

827 return cls( 

828 event_type=EventType.STEP_STARTED.value, 

829 event_timestamp=context.start_timestamp, 

830 sub_type=context.sub_type, 

831 event_id=context.event_id, 

832 operation_id=context.operation.operation_id, 

833 name=context.operation.name, 

834 parent_id=context.operation.parent_id, 

835 step_started_details=StepStartedDetails(), 

836 ) 

837 

838 @classmethod 

839 def create_step_event_succeeded(cls, context: EventCreationContext) -> Event: 

840 step_details: StepDetails | None = context.operation.step_details 

841 event_result: EventResult | None = ( 

842 EventResult.from_details(step_details, context.include_execution_data) 

843 if step_details 

844 else None 

845 ) 

846 return cls( 

847 event_type=EventType.STEP_SUCCEEDED.value, 

848 event_timestamp=context.end_timestamp, 

849 sub_type=context.sub_type, 

850 event_id=context.event_id, 

851 operation_id=context.operation.operation_id, 

852 name=context.operation.name, 

853 parent_id=context.operation.parent_id, 

854 step_succeeded_details=StepSucceededDetails( 

855 result=event_result, 

856 retry_details=context.get_retry_details(), 

857 ), 

858 ) 

859 

860 @classmethod 

861 def create_step_event_failed(cls, context: EventCreationContext) -> Event: 

862 step_details: StepDetails | None = context.operation.step_details 

863 event_error: EventError | None = ( 

864 EventError.from_details( 

865 step_details, include=context.include_execution_data 

866 ) 

867 if step_details 

868 else None 

869 ) 

870 return cls( 

871 event_type=EventType.STEP_FAILED.value, 

872 event_timestamp=context.end_timestamp, 

873 sub_type=context.sub_type, 

874 event_id=context.event_id, 

875 operation_id=context.operation.operation_id, 

876 name=context.operation.name, 

877 parent_id=context.operation.parent_id, 

878 step_failed_details=StepFailedDetails( 

879 error=event_error, 

880 retry_details=context.get_retry_details(), 

881 ), 

882 ) 

883 

884 @classmethod 

885 def create_step_event(cls, context: EventCreationContext) -> Event: 

886 """Create step event based on action.""" 

887 match context.operation.status: 

888 case OperationStatus.STARTED: 

889 return cls.create_step_event_started(context) 

890 case OperationStatus.SUCCEEDED: 

891 return cls.create_step_event_succeeded(context) 

892 case OperationStatus.FAILED: 

893 return cls.create_step_event_failed(context) 

894 case _: 

895 msg = ( 

896 f"Operation status {context.operation.status} is not valid for step operations. " 

897 f"Valid statuses are: STARTED, SUCCEEDED, FAILED" 

898 ) 

899 raise InvalidParameterValueException(msg) 

900 

901 @classmethod 

902 def create_chained_invoke_event_pending( 

903 cls, context: EventCreationContext 

904 ) -> Event: 

905 input: EventInput = EventInput.from_start_durable_execution_input( 

906 context.start_durable_execution_input, context.include_execution_data 

907 ) 

908 return cls( 

909 event_type=EventType.CHAINED_INVOKE_STARTED.value, 

910 event_timestamp=context.start_timestamp, 

911 sub_type=context.sub_type, 

912 event_id=context.event_id, 

913 operation_id=context.operation.operation_id, 

914 name=context.operation.name, 

915 parent_id=context.operation.parent_id, 

916 chained_invoke_pending_details=ChainedInvokePendingDetails( 

917 input=input, 

918 function_name=context.start_durable_execution_input.function_name, 

919 ), 

920 ) 

921 

922 @classmethod 

923 def create_chained_invoke_event_started( 

924 cls, context: EventCreationContext 

925 ) -> Event: 

926 return cls( 

927 event_type=EventType.CHAINED_INVOKE_STARTED.value, 

928 event_timestamp=context.start_timestamp, 

929 sub_type=context.sub_type, 

930 event_id=context.event_id, 

931 operation_id=context.operation.operation_id, 

932 name=context.operation.name, 

933 parent_id=context.operation.parent_id, 

934 chained_invoke_started_details=ChainedInvokeStartedDetails( 

935 durable_execution_arn=context.durable_execution_arn 

936 ), 

937 ) 

938 

939 @classmethod 

940 def create_chained_invoke_event_succeeded( 

941 cls, context: EventCreationContext 

942 ) -> Event: 

943 chained_invoke_details: ChainedInvokeDetails | None = ( 

944 context.operation.chained_invoke_details 

945 ) 

946 event_result: EventResult | None = ( 

947 EventResult.from_details( 

948 chained_invoke_details, context.include_execution_data 

949 ) 

950 if chained_invoke_details 

951 else None 

952 ) 

953 return cls( 

954 event_type=EventType.CHAINED_INVOKE_SUCCEEDED.value, 

955 event_timestamp=context.end_timestamp, 

956 sub_type=context.sub_type, 

957 event_id=context.event_id, 

958 operation_id=context.operation.operation_id, 

959 name=context.operation.name, 

960 parent_id=context.operation.parent_id, 

961 chained_invoke_succeeded_details=ChainedInvokeSucceededDetails( 

962 result=event_result 

963 ), 

964 ) 

965 

966 @classmethod 

967 def create_chained_invoke_event_failed(cls, context: EventCreationContext) -> Event: 

968 chained_invoke_details: ChainedInvokeDetails | None = ( 

969 context.operation.chained_invoke_details 

970 ) 

971 event_error: EventError | None = ( 

972 EventError.from_details( 

973 chained_invoke_details, include=context.include_execution_data 

974 ) 

975 if chained_invoke_details 

976 else None 

977 ) 

978 return cls( 

979 event_type=EventType.CHAINED_INVOKE_FAILED.value, 

980 event_timestamp=context.end_timestamp, 

981 sub_type=context.sub_type, 

982 event_id=context.event_id, 

983 operation_id=context.operation.operation_id, 

984 name=context.operation.name, 

985 parent_id=context.operation.parent_id, 

986 chained_invoke_failed_details=ChainedInvokeFailedDetails(error=event_error), 

987 ) 

988 

989 @classmethod 

990 def create_chained_invoke_event_timed_out( 

991 cls, context: EventCreationContext 

992 ) -> Event: 

993 chained_invoke_details: ChainedInvokeDetails | None = ( 

994 context.operation.chained_invoke_details 

995 ) 

996 event_error: EventError | None = ( 

997 EventError.from_details( 

998 chained_invoke_details, include=context.include_execution_data 

999 ) 

1000 if chained_invoke_details 

1001 else None 

1002 ) 

1003 return cls( 

1004 event_type=EventType.CHAINED_INVOKE_TIMED_OUT.value, 

1005 event_timestamp=context.end_timestamp, 

1006 sub_type=context.sub_type, 

1007 event_id=context.event_id, 

1008 operation_id=context.operation.operation_id, 

1009 name=context.operation.name, 

1010 parent_id=context.operation.parent_id, 

1011 chained_invoke_timed_out_details=ChainedInvokeTimedOutDetails( 

1012 error=event_error 

1013 ), 

1014 ) 

1015 

1016 @classmethod 

1017 def create_chained_invoke_event_stopped( 

1018 cls, context: EventCreationContext 

1019 ) -> Event: 

1020 chained_invoke_details: ChainedInvokeDetails | None = ( 

1021 context.operation.chained_invoke_details 

1022 ) 

1023 event_error: EventError | None = ( 

1024 EventError.from_details( 

1025 chained_invoke_details, include=context.include_execution_data 

1026 ) 

1027 if chained_invoke_details 

1028 else None 

1029 ) 

1030 return cls( 

1031 event_type=EventType.CHAINED_INVOKE_STOPPED.value, 

1032 event_timestamp=context.end_timestamp, 

1033 sub_type=context.sub_type, 

1034 event_id=context.event_id, 

1035 operation_id=context.operation.operation_id, 

1036 name=context.operation.name, 

1037 parent_id=context.operation.parent_id, 

1038 chained_invoke_stopped_details=ChainedInvokeStoppedDetails( 

1039 error=event_error 

1040 ), 

1041 ) 

1042 

1043 @classmethod 

1044 def create_chained_invoke_event(cls, context: EventCreationContext) -> Event: 

1045 """Create chained invoke event based on action.""" 

1046 match context.operation.status: 

1047 case OperationStatus.PENDING: 1047 ↛ 1048line 1047 didn't jump to line 1048 because the pattern on line 1047 never matched

1048 return cls.create_chained_invoke_event_pending(context) 

1049 case OperationStatus.STARTED: 

1050 return cls.create_chained_invoke_event_started(context) 

1051 case OperationStatus.SUCCEEDED: 

1052 return cls.create_chained_invoke_event_succeeded(context) 

1053 case OperationStatus.FAILED: 

1054 return cls.create_chained_invoke_event_failed(context) 

1055 case OperationStatus.TIMED_OUT: 

1056 return cls.create_chained_invoke_event_timed_out(context) 

1057 case OperationStatus.STOPPED: 

1058 return cls.create_chained_invoke_event_stopped(context) 

1059 case _: 

1060 msg = ( 

1061 f"Operation status {context.operation.status} is not valid for chained invoke operations. Valid statuses are: " 

1062 f"STARTED, SUCCEEDED, FAILED, TIMED_OUT, STOPPED" 

1063 ) 

1064 raise InvalidParameterValueException(msg) 

1065 

1066 @classmethod 

1067 def create_callback_event_started(cls, context: EventCreationContext) -> Event: 

1068 callback_details: CallbackDetails | None = context.operation.callback_details 

1069 callback_id: str | None = ( 

1070 callback_details.callback_id if callback_details else None 

1071 ) 

1072 callback_options: CallbackOptions | None = ( 

1073 context.operation_update.callback_options 

1074 if context.operation_update 

1075 else None 

1076 ) 

1077 timeout: int | None = ( 

1078 callback_options.timeout_seconds if callback_options else None 

1079 ) 

1080 heartbeat_timeout: int | None = ( 

1081 callback_options.heartbeat_timeout_seconds if callback_options else None 

1082 ) 

1083 return cls( 

1084 event_type=EventType.CALLBACK_STARTED.value, 

1085 event_timestamp=context.start_timestamp, 

1086 sub_type=context.sub_type, 

1087 event_id=context.event_id, 

1088 operation_id=context.operation.operation_id, 

1089 name=context.operation.name, 

1090 parent_id=context.operation.parent_id, 

1091 callback_started_details=CallbackStartedDetails( 

1092 callback_id=callback_id, 

1093 timeout=timeout, 

1094 heartbeat_timeout=heartbeat_timeout, 

1095 ), 

1096 ) 

1097 

1098 @classmethod 

1099 def create_callback_event_succeeded(cls, context: EventCreationContext) -> Event: 

1100 callback_details: CallbackDetails | None = context.operation.callback_details 

1101 event_result: EventResult | None = ( 

1102 EventResult.from_details(callback_details, context.include_execution_data) 

1103 if callback_details 

1104 else None 

1105 ) 

1106 return cls( 

1107 event_type=EventType.CALLBACK_SUCCEEDED.value, 

1108 event_timestamp=context.end_timestamp, 

1109 sub_type=context.sub_type, 

1110 event_id=context.event_id, 

1111 operation_id=context.operation.operation_id, 

1112 name=context.operation.name, 

1113 parent_id=context.operation.parent_id, 

1114 callback_succeeded_details=CallbackSucceededDetails(result=event_result), 

1115 ) 

1116 

1117 @classmethod 

1118 def create_callback_event_failed(cls, context: EventCreationContext) -> Event: 

1119 callback_details: CallbackDetails | None = context.operation.callback_details 

1120 event_error: EventError | None = ( 

1121 EventError.from_details(callback_details) if callback_details else None 

1122 ) 

1123 return cls( 

1124 event_type=EventType.CALLBACK_FAILED.value, 

1125 event_timestamp=context.end_timestamp, 

1126 sub_type=context.sub_type, 

1127 event_id=context.event_id, 

1128 operation_id=context.operation.operation_id, 

1129 name=context.operation.name, 

1130 parent_id=context.operation.parent_id, 

1131 callback_failed_details=CallbackFailedDetails(error=event_error), 

1132 ) 

1133 

1134 @classmethod 

1135 def create_callback_event_timed_out(cls, context: EventCreationContext) -> Event: 

1136 callback_details: CallbackDetails | None = context.operation.callback_details 

1137 event_error: EventError | None = ( 

1138 EventError.from_details(callback_details) if callback_details else None 

1139 ) 

1140 return cls( 

1141 event_type=EventType.CALLBACK_TIMED_OUT.value, 

1142 event_timestamp=context.end_timestamp, 

1143 sub_type=context.sub_type, 

1144 event_id=context.event_id, 

1145 operation_id=context.operation.operation_id, 

1146 name=context.operation.name, 

1147 parent_id=context.operation.parent_id, 

1148 callback_timed_out_details=CallbackTimedOutDetails(error=event_error), 

1149 ) 

1150 

1151 @classmethod 

1152 def create_callback_event(cls, context: EventCreationContext) -> Event: 

1153 """Create callback event based on action.""" 

1154 match context.operation.status: 

1155 case OperationStatus.STARTED: 

1156 return cls.create_callback_event_started(context) 

1157 case OperationStatus.SUCCEEDED: 

1158 return cls.create_callback_event_succeeded(context) 

1159 case OperationStatus.FAILED: 

1160 return cls.create_callback_event_failed(context) 

1161 case OperationStatus.TIMED_OUT: 

1162 return cls.create_callback_event_timed_out(context) 

1163 case _: 

1164 msg = ( 

1165 f"Operation status {context.operation.status} is not valid for callback operations. " 

1166 f"Valid statuses are: STARTED, SUCCEEDED, FAILED, TIMED_OUT" 

1167 ) 

1168 raise InvalidParameterValueException(msg) 

1169 

1170 @classmethod 

1171 def create_invocation_completed( 

1172 cls, 

1173 event_id: int, 

1174 event_timestamp: datetime.datetime, 

1175 start_timestamp: datetime.datetime, 

1176 end_timestamp: datetime.datetime, 

1177 request_id: str, 

1178 ) -> Event: 

1179 """Create invocation completed event.""" 

1180 return cls( 

1181 event_type=EventType.INVOCATION_COMPLETED.value, 

1182 event_timestamp=event_timestamp, 

1183 event_id=event_id, 

1184 invocation_completed_details=InvocationCompletedDetails( 

1185 start_timestamp=start_timestamp, 

1186 end_timestamp=end_timestamp, 

1187 request_id=request_id, 

1188 ), 

1189 ) 

1190 

1191 @classmethod 

1192 def create_event_started(cls, context: EventCreationContext) -> Event: 

1193 """Convert operation to started event.""" 

1194 if context.operation.start_timestamp is None: 

1195 msg: str = "Operation start timestamp cannot be None when converting to started event" 

1196 raise InvalidParameterValueException(msg) 

1197 

1198 match context.operation.operation_type: 

1199 case OperationType.EXECUTION: 

1200 return cls.create_execution_event_started(context) 

1201 case OperationType.CONTEXT: 

1202 return cls.create_context_event_started(context) 

1203 case OperationType.WAIT: 

1204 return cls.create_wait_event_started(context) 

1205 case OperationType.STEP: 

1206 return cls.create_step_event_started(context) 

1207 case OperationType.CHAINED_INVOKE: 

1208 return cls.create_chained_invoke_event_started(context) 

1209 case OperationType.CALLBACK: 

1210 return cls.create_callback_event_started(context) 

1211 case _: 

1212 msg = f"Unknown operation type: {context.operation.operation_type}" 

1213 raise InvalidParameterValueException(msg) 

1214 

1215 @classmethod 

1216 def from_event_with_id(cls, event: Event, event_id: int) -> Event: 

1217 """Create a new Event from an existing event with updated event_id.""" 

1218 return cls( 

1219 event_type=event.event_type, 

1220 event_timestamp=event.event_timestamp, 

1221 sub_type=event.sub_type, 

1222 event_id=event_id, 

1223 operation_id=event.operation_id, 

1224 name=event.name, 

1225 parent_id=event.parent_id, 

1226 execution_started_details=event.execution_started_details, 

1227 execution_succeeded_details=event.execution_succeeded_details, 

1228 execution_failed_details=event.execution_failed_details, 

1229 execution_timed_out_details=event.execution_timed_out_details, 

1230 execution_stopped_details=event.execution_stopped_details, 

1231 context_started_details=event.context_started_details, 

1232 context_succeeded_details=event.context_succeeded_details, 

1233 context_failed_details=event.context_failed_details, 

1234 wait_started_details=event.wait_started_details, 

1235 wait_succeeded_details=event.wait_succeeded_details, 

1236 wait_cancelled_details=event.wait_cancelled_details, 

1237 step_started_details=event.step_started_details, 

1238 step_succeeded_details=event.step_succeeded_details, 

1239 step_failed_details=event.step_failed_details, 

1240 chained_invoke_pending_details=event.chained_invoke_pending_details, 

1241 chained_invoke_started_details=event.chained_invoke_started_details, 

1242 chained_invoke_succeeded_details=event.chained_invoke_succeeded_details, 

1243 chained_invoke_failed_details=event.chained_invoke_failed_details, 

1244 chained_invoke_timed_out_details=event.chained_invoke_timed_out_details, 

1245 chained_invoke_stopped_details=event.chained_invoke_stopped_details, 

1246 callback_started_details=event.callback_started_details, 

1247 callback_succeeded_details=event.callback_succeeded_details, 

1248 callback_failed_details=event.callback_failed_details, 

1249 callback_timed_out_details=event.callback_timed_out_details, 

1250 ) 

1251 

1252 @classmethod 

1253 def create_event_terminated(cls, context: EventCreationContext) -> Event: 

1254 """Convert operation to finished event.""" 

1255 operation: Operation = context.operation 

1256 if operation.end_timestamp is None: 

1257 msg: str = "Operation end timestamp cannot be None when converting to finished event" 

1258 raise InvalidParameterValueException(msg) 

1259 

1260 if operation.status not in TERMINAL_STATUSES: 

1261 msg = f"Operation status must be one of SUCCEEDED, FAILED, TIMED_OUT, STOPPED, or CANCELLED. Got: {operation.status}" 

1262 raise InvalidParameterValueException(msg) 

1263 

1264 match operation.operation_type: 

1265 case OperationType.EXECUTION: 

1266 return cls.create_execution_event(context) 

1267 case OperationType.CONTEXT: 

1268 return cls.create_context_event(context) 

1269 case OperationType.WAIT: 

1270 return cls.create_wait_event(context) 

1271 case OperationType.STEP: 

1272 return cls.create_step_event(context) 

1273 case OperationType.CHAINED_INVOKE: 

1274 return cls.create_chained_invoke_event(context) 

1275 case OperationType.CALLBACK: 

1276 return cls.create_callback_event(context) 

1277 case _: 

1278 msg = f"Unknown operation type: {operation.operation_type}" 

1279 raise InvalidParameterValueException(msg) 

1280 

1281 

1282@dataclass(frozen=True) 

1283class HistoryEventTypeConfig: 

1284 """Configuration for how to process a specific event type.""" 

1285 

1286 operation_type: OperationType | None 

1287 operation_status: OperationStatus | None 

1288 is_start_event: bool 

1289 is_end_event: bool 

1290 has_result: bool # Whether this event type contains result/error data 

1291 

1292 

1293# Mapping of event types to their processing configuration 

1294# This matches the TypeScript historyEventTypes constant 

1295HISTORY_EVENT_TYPES: dict[str, HistoryEventTypeConfig] = { 

1296 "ExecutionStarted": HistoryEventTypeConfig( 

1297 operation_type=OperationType.EXECUTION, 

1298 operation_status=OperationStatus.STARTED, 

1299 is_start_event=True, 

1300 is_end_event=False, 

1301 has_result=False, 

1302 ), 

1303 "ExecutionFailed": HistoryEventTypeConfig( 

1304 operation_type=OperationType.EXECUTION, 

1305 operation_status=OperationStatus.FAILED, 

1306 is_start_event=False, 

1307 is_end_event=True, 

1308 has_result=False, 

1309 ), 

1310 "ExecutionStopped": HistoryEventTypeConfig( 

1311 operation_type=OperationType.EXECUTION, 

1312 operation_status=OperationStatus.STOPPED, 

1313 is_start_event=False, 

1314 is_end_event=True, 

1315 has_result=False, 

1316 ), 

1317 "ExecutionSucceeded": HistoryEventTypeConfig( 

1318 operation_type=OperationType.EXECUTION, 

1319 operation_status=OperationStatus.SUCCEEDED, 

1320 is_start_event=False, 

1321 is_end_event=True, 

1322 has_result=False, 

1323 ), 

1324 "ExecutionTimedOut": HistoryEventTypeConfig( 

1325 operation_type=OperationType.EXECUTION, 

1326 operation_status=OperationStatus.TIMED_OUT, 

1327 is_start_event=False, 

1328 is_end_event=True, 

1329 has_result=False, 

1330 ), 

1331 "CallbackStarted": HistoryEventTypeConfig( 

1332 operation_type=OperationType.CALLBACK, 

1333 operation_status=OperationStatus.STARTED, 

1334 is_start_event=True, 

1335 is_end_event=False, 

1336 has_result=False, 

1337 ), 

1338 "CallbackFailed": HistoryEventTypeConfig( 

1339 operation_type=OperationType.CALLBACK, 

1340 operation_status=OperationStatus.FAILED, 

1341 is_start_event=False, 

1342 is_end_event=True, 

1343 has_result=True, 

1344 ), 

1345 "CallbackSucceeded": HistoryEventTypeConfig( 

1346 operation_type=OperationType.CALLBACK, 

1347 operation_status=OperationStatus.SUCCEEDED, 

1348 is_start_event=False, 

1349 is_end_event=True, 

1350 has_result=True, 

1351 ), 

1352 "CallbackTimedOut": HistoryEventTypeConfig( 

1353 operation_type=OperationType.CALLBACK, 

1354 operation_status=OperationStatus.TIMED_OUT, 

1355 is_start_event=False, 

1356 is_end_event=True, 

1357 has_result=True, 

1358 ), 

1359 "ContextStarted": HistoryEventTypeConfig( 

1360 operation_type=OperationType.CONTEXT, 

1361 operation_status=OperationStatus.STARTED, 

1362 is_start_event=True, 

1363 is_end_event=False, 

1364 has_result=False, 

1365 ), 

1366 "ContextFailed": HistoryEventTypeConfig( 

1367 operation_type=OperationType.CONTEXT, 

1368 operation_status=OperationStatus.FAILED, 

1369 is_start_event=False, 

1370 is_end_event=True, 

1371 has_result=True, 

1372 ), 

1373 "ContextSucceeded": HistoryEventTypeConfig( 

1374 operation_type=OperationType.CONTEXT, 

1375 operation_status=OperationStatus.SUCCEEDED, 

1376 is_start_event=False, 

1377 is_end_event=True, 

1378 has_result=True, 

1379 ), 

1380 "ChainedInvokeStarted": HistoryEventTypeConfig( 

1381 operation_type=OperationType.CHAINED_INVOKE, 

1382 operation_status=OperationStatus.STARTED, 

1383 is_start_event=True, 

1384 is_end_event=False, 

1385 has_result=False, 

1386 ), 

1387 "ChainedInvokeFailed": HistoryEventTypeConfig( 

1388 operation_type=OperationType.CHAINED_INVOKE, 

1389 operation_status=OperationStatus.FAILED, 

1390 is_start_event=False, 

1391 is_end_event=True, 

1392 has_result=True, 

1393 ), 

1394 "ChainedInvokeSucceeded": HistoryEventTypeConfig( 

1395 operation_type=OperationType.CHAINED_INVOKE, 

1396 operation_status=OperationStatus.SUCCEEDED, 

1397 is_start_event=False, 

1398 is_end_event=True, 

1399 has_result=True, 

1400 ), 

1401 "ChainedInvokeTimedOut": HistoryEventTypeConfig( 

1402 operation_type=OperationType.CHAINED_INVOKE, 

1403 operation_status=OperationStatus.TIMED_OUT, 

1404 is_start_event=False, 

1405 is_end_event=True, 

1406 has_result=True, 

1407 ), 

1408 "ChainedInvokeCancelled": HistoryEventTypeConfig( 

1409 operation_type=OperationType.CHAINED_INVOKE, 

1410 operation_status=OperationStatus.CANCELLED, 

1411 is_start_event=False, 

1412 is_end_event=True, 

1413 has_result=True, 

1414 ), 

1415 "StepStarted": HistoryEventTypeConfig( 

1416 operation_type=OperationType.STEP, 

1417 operation_status=OperationStatus.STARTED, 

1418 is_start_event=True, 

1419 is_end_event=False, 

1420 has_result=False, 

1421 ), 

1422 "StepFailed": HistoryEventTypeConfig( 

1423 operation_type=OperationType.STEP, 

1424 operation_status=OperationStatus.FAILED, 

1425 is_start_event=False, 

1426 is_end_event=True, 

1427 has_result=True, 

1428 ), 

1429 "StepSucceeded": HistoryEventTypeConfig( 

1430 operation_type=OperationType.STEP, 

1431 operation_status=OperationStatus.SUCCEEDED, 

1432 is_start_event=False, 

1433 is_end_event=True, 

1434 has_result=True, 

1435 ), 

1436 "WaitStarted": HistoryEventTypeConfig( 

1437 operation_type=OperationType.WAIT, 

1438 operation_status=OperationStatus.STARTED, 

1439 is_start_event=True, 

1440 is_end_event=False, 

1441 has_result=True, 

1442 ), 

1443 "WaitSucceeded": HistoryEventTypeConfig( 

1444 operation_type=OperationType.WAIT, 

1445 operation_status=OperationStatus.SUCCEEDED, 

1446 is_start_event=False, 

1447 is_end_event=True, 

1448 has_result=True, 

1449 ), 

1450 "WaitCancelled": HistoryEventTypeConfig( 

1451 operation_type=OperationType.WAIT, 

1452 operation_status=OperationStatus.CANCELLED, 

1453 is_start_event=False, 

1454 is_end_event=True, 

1455 has_result=True, 

1456 ), 

1457 # TODO: add support for populating invocation information from InvocationCompleted event 

1458 "InvocationCompleted": HistoryEventTypeConfig( 

1459 operation_type=None, 

1460 operation_status=None, 

1461 is_start_event=False, 

1462 is_end_event=False, 

1463 has_result=True, 

1464 ), 

1465} 

1466 

1467 

1468def events_to_operations(events: list[Event]) -> list[Operation]: 

1469 """Convert a list of history events into operations. 

1470 

1471 This function processes raw history events and groups them by operation ID, 

1472 creating comprehensive operation objects following the TypeScript pattern from 

1473 aws-durable-execution-sdk-js-testing. 

1474 

1475 Multiple events for the same operation_id are merged together, with each event 

1476 contributing its specific fields (e.g., CallbackStarted provides callback_id, 

1477 CallbackSucceeded provides result). 

1478 

1479 Args: 

1480 events: List of history events to process 

1481 

1482 Returns: 

1483 List of operations, one per unique operation ID 

1484 

1485 Raises: 

1486 InvalidParameterValueException: When required fields are missing from an event 

1487 

1488 Note: 

1489 InvocationCompleted events are currently skipped as they don't represent 

1490 operations. Future enhancement: populate invocation information from these 

1491 events (TODO). 

1492 """ 

1493 operations_map: dict[str, Operation] = {} 

1494 

1495 for event in events: 

1496 if not event.event_type: 

1497 msg = "Missing required 'event_type' field in event" 

1498 raise InvalidParameterValueException(msg) 

1499 

1500 # Get event type configuration 

1501 event_config: HistoryEventTypeConfig | None = HISTORY_EVENT_TYPES.get( 

1502 event.event_type 

1503 ) 

1504 if not event_config: 

1505 msg = f"Unknown event type: {event.event_type}" 

1506 raise InvalidParameterValueException(msg) 

1507 

1508 # TODO: add support for populating invocation information from InvocationCompleted event 

1509 if event.event_type == "InvocationCompleted": 

1510 continue 

1511 

1512 if not event.operation_id: 

1513 msg = f"Missing required 'operation_id' field in event {event.event_id}" 

1514 raise InvalidParameterValueException(msg) 

1515 

1516 # Get previous operation if it exists 

1517 previous_operation: Operation | None = operations_map.get(event.operation_id) 

1518 

1519 # Get operation type and status from configuration 

1520 operation_type: OperationType = ( 

1521 event_config.operation_type or OperationType.EXECUTION 

1522 ) 

1523 status: OperationStatus = ( 

1524 event_config.operation_status or OperationStatus.PENDING 

1525 ) 

1526 

1527 # Parse sub_type 

1528 sub_type: OperationSubType | str | None = None 

1529 if event.sub_type: 

1530 try: 

1531 sub_type = OperationSubType(event.sub_type) 

1532 except ValueError: 

1533 sub_type = event.sub_type 

1534 

1535 # Create base operation 

1536 operation = Operation( 

1537 operation_id=event.operation_id, 

1538 operation_type=operation_type, 

1539 status=status, 

1540 name=event.name, 

1541 parent_id=event.parent_id, 

1542 sub_type=sub_type, 

1543 start_timestamp=datetime.datetime.now(tz=datetime.timezone.utc), 

1544 ) 

1545 

1546 # Merge with previous operation if it exists 

1547 # Most fields are immutable, so they get preserved from previous events 

1548 if previous_operation: 

1549 operation = replace( 

1550 operation, 

1551 name=operation.name or previous_operation.name, 

1552 parent_id=operation.parent_id or previous_operation.parent_id, 

1553 sub_type=operation.sub_type or previous_operation.sub_type, 

1554 start_timestamp=previous_operation.start_timestamp, 

1555 end_timestamp=previous_operation.end_timestamp, 

1556 execution_details=previous_operation.execution_details, 

1557 context_details=previous_operation.context_details, 

1558 step_details=previous_operation.step_details, 

1559 wait_details=previous_operation.wait_details, 

1560 callback_details=previous_operation.callback_details, 

1561 chained_invoke_details=previous_operation.chained_invoke_details, 

1562 ) 

1563 

1564 # Set timestamps based on event configuration 

1565 if event_config.is_start_event: 

1566 operation = replace(operation, start_timestamp=event.event_timestamp) 

1567 if event_config.is_end_event: 

1568 operation = replace(operation, end_timestamp=event.event_timestamp) 

1569 

1570 # Add operation-specific details incrementally 

1571 # Each event type contributes only the fields it has 

1572 

1573 # EXECUTION details 

1574 if ( 

1575 operation_type == OperationType.EXECUTION 

1576 and event.execution_started_details 

1577 and event.execution_started_details.input 

1578 ): 

1579 operation = replace( 

1580 operation, 

1581 execution_details=ExecutionDetails( 

1582 input_payload=event.execution_started_details.input.payload 

1583 ), 

1584 ) 

1585 

1586 # CALLBACK details - merge callback_id, result, and error from different events 

1587 if operation_type == OperationType.CALLBACK: 

1588 existing_cb: CallbackDetails | None = operation.callback_details 

1589 callback_id: str = existing_cb.callback_id if existing_cb else "" 

1590 result: str | None = existing_cb.result if existing_cb else None 

1591 error: ErrorObject | None = existing_cb.error if existing_cb else None 

1592 

1593 # CallbackStarted provides callback_id 

1594 if event.callback_started_details: 

1595 callback_id = event.callback_started_details.callback_id or callback_id 

1596 

1597 # CallbackSucceeded provides result 

1598 if ( 

1599 event.callback_succeeded_details 

1600 and event.callback_succeeded_details.result 

1601 ): 

1602 result = event.callback_succeeded_details.result.payload 

1603 

1604 # CallbackFailed provides error 

1605 if event.callback_failed_details and event.callback_failed_details.error: 

1606 error = event.callback_failed_details.error.payload 

1607 

1608 # CallbackTimedOut provides error 

1609 if ( 

1610 event.callback_timed_out_details 

1611 and event.callback_timed_out_details.error 

1612 ): 

1613 error = event.callback_timed_out_details.error.payload 

1614 

1615 operation = replace( 

1616 operation, 

1617 callback_details=CallbackDetails( 

1618 callback_id=callback_id, 

1619 result=result, 

1620 error=error, 

1621 ), 

1622 ) 

1623 

1624 # STEP details - only update if this event type has result data 

1625 if operation_type == OperationType.STEP and event_config.has_result: 

1626 existing_step: StepDetails | None = operation.step_details 

1627 result_val: str | None = existing_step.result if existing_step else None 

1628 error_val: ErrorObject | None = ( 

1629 existing_step.error if existing_step else None 

1630 ) 

1631 attempt: int = existing_step.attempt if existing_step else 0 

1632 next_attempt_ts: datetime.datetime | None = ( 

1633 existing_step.next_attempt_timestamp if existing_step else None 

1634 ) 

1635 

1636 # StepSucceeded provides result 

1637 if event.step_succeeded_details: 

1638 if event.step_succeeded_details.result: 1638 ↛ 1640line 1638 didn't jump to line 1640 because the condition on line 1638 was always true

1639 result_val = event.step_succeeded_details.result.payload 

1640 if event.step_succeeded_details.retry_details: 

1641 attempt = event.step_succeeded_details.retry_details.current_attempt 

1642 

1643 # StepFailed provides error and retry details 

1644 if event.step_failed_details: 

1645 if event.step_failed_details.error: 1645 ↛ 1647line 1645 didn't jump to line 1647 because the condition on line 1645 was always true

1646 error_val = event.step_failed_details.error.payload 

1647 if event.step_failed_details.retry_details: 1647 ↛ 1657line 1647 didn't jump to line 1657 because the condition on line 1647 was always true

1648 attempt = event.step_failed_details.retry_details.current_attempt 

1649 if ( 1649 ↛ 1657line 1649 didn't jump to line 1657 because the condition on line 1649 was always true

1650 event.step_failed_details.retry_details.next_attempt_delay_seconds 

1651 is not None 

1652 ): 

1653 next_attempt_ts = event.event_timestamp + datetime.timedelta( 

1654 seconds=event.step_failed_details.retry_details.next_attempt_delay_seconds 

1655 ) 

1656 

1657 operation = replace( 

1658 operation, 

1659 step_details=StepDetails( 

1660 result=result_val, 

1661 error=error_val, 

1662 attempt=attempt, 

1663 next_attempt_timestamp=next_attempt_ts, 

1664 ), 

1665 ) 

1666 

1667 # WAIT details 

1668 if operation_type == OperationType.WAIT and event.wait_started_details: 

1669 operation = replace( 

1670 operation, 

1671 wait_details=WaitDetails( 

1672 scheduled_end_timestamp=event.wait_started_details.scheduled_end_timestamp 

1673 ), 

1674 ) 

1675 

1676 # CONTEXT details - only update if this event type has result data (matching TypeScript hasResult) 

1677 if operation_type == OperationType.CONTEXT and event_config.has_result: 

1678 if ( 

1679 event.context_succeeded_details 

1680 and event.context_succeeded_details.result 

1681 ): 

1682 operation = replace( 

1683 operation, 

1684 context_details=ContextDetails( 

1685 result=event.context_succeeded_details.result.payload, 

1686 error=None, 

1687 ), 

1688 ) 

1689 elif event.context_failed_details and event.context_failed_details.error: 1689 ↛ 1699line 1689 didn't jump to line 1699 because the condition on line 1689 was always true

1690 operation = replace( 

1691 operation, 

1692 context_details=ContextDetails( 

1693 result=None, 

1694 error=event.context_failed_details.error.payload, 

1695 ), 

1696 ) 

1697 

1698 # CHAINED_INVOKE details - only update if this event type has result data (matching TypeScript hasResult) 

1699 if operation_type == OperationType.CHAINED_INVOKE and event_config.has_result: 

1700 if ( 

1701 event.chained_invoke_succeeded_details 

1702 and event.chained_invoke_succeeded_details.result 

1703 ): 

1704 operation = replace( 

1705 operation, 

1706 chained_invoke_details=ChainedInvokeDetails( 

1707 result=event.chained_invoke_succeeded_details.result.payload, 

1708 error=None, 

1709 ), 

1710 ) 

1711 elif ( 1711 ↛ 1724line 1711 didn't jump to line 1724 because the condition on line 1711 was always true

1712 event.chained_invoke_failed_details 

1713 and event.chained_invoke_failed_details.error 

1714 ): 

1715 operation = replace( 

1716 operation, 

1717 chained_invoke_details=ChainedInvokeDetails( 

1718 result=None, 

1719 error=event.chained_invoke_failed_details.error.payload, 

1720 ), 

1721 ) 

1722 

1723 # Store in map 

1724 operations_map[event.operation_id] = operation 

1725 

1726 return list(operations_map.values()) 

1727 

1728 

1729@dataclass(frozen=True) 

1730class GetDurableExecutionHistoryResponse(AwsApiModel): 

1731 """Response containing durable execution history events.""" 

1732 

1733 events: list[Event] = field(default_factory=list, metadata={"alias": "Events"}) 

1734 next_marker: str | None = field(default=None, metadata={"alias": "NextMarker"}) 

1735 

1736 

1737class _ExecutionResultSource(Protocol): 

1738 operations: list[Operation] 

1739 result: DurableExecutionInvocationOutput | None 

1740 

1741 

1742@dataclass(frozen=True) 

1743class DurableFunctionTestResult: 

1744 status: InvocationStatus 

1745 operations: list[Operation] 

1746 result: OperationPayload | None = None 

1747 error: ErrorObject | None = None 

1748 _all_operations: list[Operation] = field( 

1749 default_factory=list, 

1750 repr=False, 

1751 compare=False, 

1752 ) 

1753 

1754 @classmethod 

1755 def create(cls, execution: _ExecutionResultSource) -> DurableFunctionTestResult: 

1756 operations = [] 

1757 for operation in execution.operations: 

1758 if operation.operation_type is OperationType.EXECUTION: 

1759 # don't want the EXECUTION operations in the list test code asserts against 

1760 continue 

1761 

1762 if operation.parent_id is None: 

1763 operations.append(operation) 

1764 

1765 if execution.result is None: 

1766 msg: str = "Execution result must exist to create test result." 

1767 raise DurableFunctionsTestError(msg) 

1768 

1769 return cls( 

1770 status=execution.result.status, 

1771 operations=operations, 

1772 result=execution.result.result, 

1773 error=execution.result.error, 

1774 _all_operations=execution.operations, 

1775 ) 

1776 

1777 @classmethod 

1778 def from_execution_history( 

1779 cls, 

1780 execution_response: GetDurableExecutionResponse, 

1781 history_response: GetDurableExecutionHistoryResponse, 

1782 ) -> DurableFunctionTestResult: 

1783 """Create test result from execution history responses. 

1784 

1785 Factory method for cloud runner that builds DurableFunctionTestResult 

1786 from GetDurableExecution and GetDurableExecutionHistory API responses. 

1787 """ 

1788 # Map status string to InvocationStatus enum 

1789 try: 

1790 status = InvocationStatus[execution_response.status] 

1791 except KeyError: 

1792 logger.warning( 

1793 "Unknown status: %s, defaulting to FAILED", execution_response.status 

1794 ) 

1795 status = InvocationStatus.FAILED 

1796 

1797 # Convert Events to Operations - group by operation_id and merge 

1798 try: 

1799 svc_operations = events_to_operations(history_response.events) 

1800 except Exception as e: 

1801 logger.warning("Failed to convert events to operations: %s", e) 

1802 svc_operations = [] 

1803 

1804 # Build top-level operation list (exclude EXECUTION type) 

1805 operations = [] 

1806 for svc_op in svc_operations: 

1807 if svc_op.operation_type == OperationType.EXECUTION: 

1808 continue 

1809 if svc_op.parent_id is None: 

1810 operations.append(svc_op) 

1811 

1812 return cls( 

1813 status=status, 

1814 operations=operations, 

1815 result=execution_response.result, 

1816 error=execution_response.error, 

1817 _all_operations=svc_operations, 

1818 ) 

1819 

1820 def get_operation_by_name(self, name: str) -> Operation: 

1821 for operation in self.operations: 

1822 if operation.name == name: 

1823 return operation 

1824 msg: str = f"Operation with name '{name}' not found" 

1825 raise DurableFunctionsTestError(msg) 

1826 

1827 def get_step(self, name: str) -> Operation: 

1828 return self._get_operation_by_name_and_type(name, OperationType.STEP) 

1829 

1830 def get_wait(self, name: str) -> Operation: 

1831 return self._get_operation_by_name_and_type(name, OperationType.WAIT) 

1832 

1833 def get_context(self, name: str) -> Operation: 

1834 return self._get_operation_by_name_and_type(name, OperationType.CONTEXT) 

1835 

1836 def get_callback(self, name: str) -> Operation: 

1837 return self._get_operation_by_name_and_type(name, OperationType.CALLBACK) 

1838 

1839 def get_invoke(self, name: str) -> Operation: 

1840 return self._get_operation_by_name_and_type(name, OperationType.CHAINED_INVOKE) 

1841 

1842 def get_execution(self, name: str) -> Operation: 

1843 return self._get_operation_by_name_and_type(name, OperationType.EXECUTION) 

1844 

1845 def get_deserialized_result(self, serdes: ExtendedTypeSerDes | None = None) -> Any: 

1846 """Return the deserialized execution result.""" 

1847 return _deserialize_operation_payload(self.result, serdes) 

1848 

1849 def get_operation_deserialized_result( 

1850 self, 

1851 operation: Operation, 

1852 serdes: ExtendedTypeSerDes | None = None, 

1853 ) -> Any: 

1854 """Return the deserialized result payload for a service operation.""" 

1855 match operation.operation_type: 

1856 case OperationType.CONTEXT: 

1857 result = ( 

1858 operation.context_details.result 

1859 if operation.context_details 

1860 else None 

1861 ) 

1862 case OperationType.STEP: 

1863 result = ( 

1864 operation.step_details.result if operation.step_details else None 

1865 ) 

1866 case OperationType.CALLBACK: 

1867 result = ( 

1868 operation.callback_details.result 

1869 if operation.callback_details 

1870 else None 

1871 ) 

1872 case OperationType.CHAINED_INVOKE: 

1873 result = ( 

1874 operation.chained_invoke_details.result 

1875 if operation.chained_invoke_details 

1876 else None 

1877 ) 

1878 case _: 

1879 result = None 

1880 return _deserialize_operation_payload(result, serdes) 

1881 

1882 def get_child_operations(self, operation: Operation) -> list[Operation]: 

1883 """Return direct child operations for a service operation.""" 

1884 return [ 

1885 candidate 

1886 for candidate in self._operation_source() 

1887 if candidate.parent_id == operation.operation_id 

1888 ] 

1889 

1890 def get_all_operations(self) -> list[Operation]: 

1891 """Return all non-execution operations, including nested operations.""" 

1892 return [ 

1893 operation 

1894 for operation in self._operation_source() 

1895 if operation.operation_type != OperationType.EXECUTION 

1896 ] 

1897 

1898 def _operation_source(self) -> list[Operation]: 

1899 return self._all_operations or self.operations 

1900 

1901 def _get_operation_by_name_and_type( 

1902 self, name: str, operation_type: OperationType 

1903 ) -> Operation: 

1904 operation = self.get_operation_by_name(name) 

1905 if operation.operation_type != operation_type: 

1906 msg = ( 

1907 f"Operation with name '{name}' has type " 

1908 f"{operation.operation_type}, expected {operation_type}" 

1909 ) 

1910 raise DurableFunctionsTestError(msg) 

1911 return operation 

1912 

1913 

1914def _deserialize_operation_payload( 

1915 payload: OperationPayload | None, 

1916 serdes: ExtendedTypeSerDes | None = None, 

1917) -> Any: 

1918 """Deserialize an operation payload using the provided or default serializer.""" 

1919 if not payload: 

1920 return None 

1921 

1922 if serdes is None: 1922 ↛ 1925line 1922 didn't jump to line 1925 because the condition on line 1922 was always true

1923 serdes = ExtendedTypeSerDes() 

1924 

1925 try: 

1926 return serdes.deserialize_sync(payload) 

1927 except Exception: 

1928 return json.loads(payload) 

1929 

1930 

1931def _get_callback_id_from_events( 

1932 events: list[Event], name: str | None = None 

1933) -> str | None: 

1934 """ 

1935 Get callback ID from execution history for callbacks that haven't completed. 

1936 

1937 Args: 

1938 execution_arn: The ARN of the execution to query. 

1939 name: Optional callback name to search for. If not provided, returns the latest callback. 

1940 

1941 Returns: 

1942 The callback ID string for a non-completed callback whose creating 

1943 invocation has completed, or None if not found. 

1944 

1945 Raises: 

1946 DurableFunctionsTestError: If the named callback has already succeeded/failed/timed out. 

1947 """ 

1948 callback_started_events = [ 

1949 event for event in events if event.event_type == "CallbackStarted" 

1950 ] 

1951 

1952 if not callback_started_events: 

1953 return None 

1954 

1955 completed_callback_ids = { 

1956 event.event_id 

1957 for event in events 

1958 if event.event_type 

1959 in ["CallbackSucceeded", "CallbackFailed", "CallbackTimedOut"] 

1960 } 

1961 

1962 def is_callback_ready(callback_started_event: Event) -> bool: 

1963 for event in events[events.index(callback_started_event) + 1 :]: 

1964 if event.event_type == "InvocationCompleted": 

1965 return True 

1966 return False 

1967 

1968 if name is not None: 

1969 for event in callback_started_events: 

1970 if event.name == name: 

1971 callback_id = event.event_id 

1972 if callback_id in completed_callback_ids: 

1973 raise DurableFunctionsTestError( 

1974 f"Callback {name} has already completed (succeeded/failed/timed out)" 

1975 ) 

1976 if not is_callback_ready(event): 

1977 return None 

1978 return ( 

1979 event.callback_started_details.callback_id 

1980 if event.callback_started_details 

1981 else None 

1982 ) 

1983 return None 

1984 

1985 # If name is not provided, find the latest non-completed callback event 

1986 active_callbacks = [ 

1987 event 

1988 for event in callback_started_events 

1989 if event.event_id not in completed_callback_ids and is_callback_ready(event) 

1990 ] 

1991 

1992 if not active_callbacks: 

1993 return None 

1994 

1995 latest_event = active_callbacks[-1] 

1996 return ( 

1997 latest_event.callback_started_details.callback_id 

1998 if latest_event.callback_started_details 

1999 else None 

2000 ) 

2001 

2002 

2003@dataclass(frozen=True) 

2004class InvokeResponse: 

2005 """Response from invoking a durable function.""" 

2006 

2007 invocation_output: DurableExecutionInvocationOutput 

2008 request_id: str