python-saga-orchestrator 0.2.4__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/PKG-INFO +1 -1
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/python_saga_orchestrator.egg-info/PKG-INFO +1 -1
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/python_saga_orchestrator.egg-info/SOURCES.txt +1 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/_version.py +2 -2
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/engine.py +100 -20
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/repository.py +1 -1
- python_saga_orchestrator-0.4.0/saga_orchestrator/domain/models/enums/base_str_enum.py +10 -0
- python_saga_orchestrator-0.4.0/saga_orchestrator/domain/models/enums/saga_status.py +18 -0
- python_saga_orchestrator-0.4.0/saga_orchestrator/domain/models/enums/saga_step_phase.py +9 -0
- python_saga_orchestrator-0.4.0/saga_orchestrator/domain/models/enums/saga_step_status.py +10 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/step.py +16 -7
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/helpers.py +8 -1
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_admin_api.py +93 -6
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_core_flow.py +49 -1
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_input_context.py +3 -3
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_step_type_resolution.py +11 -5
- python_saga_orchestrator-0.2.4/saga_orchestrator/domain/models/enums/saga_status.py +0 -15
- python_saga_orchestrator-0.2.4/saga_orchestrator/domain/models/enums/saga_step_phase.py +0 -7
- python_saga_orchestrator-0.2.4/saga_orchestrator/domain/models/enums/saga_step_status.py +0 -7
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/.github/workflows/ci.yml +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/.github/workflows/publish.yml +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/.gitignore +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/Dockerfile +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/LICENSE +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/Makefile +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/README.md +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/docker-compose.yaml +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/admin_skip.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/common.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/compensation_flow.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/http_and_queue.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/llm_deploy.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/retry_recovery.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/pyproject.toml +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/python_saga_orchestrator.egg-info/dependency_links.txt +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/python_saga_orchestrator.egg-info/requires.txt +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/python_saga_orchestrator.egg-info/top_level.txt +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/admin/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/admin/api.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/builder.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/orchestrator.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/exceptions/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/exceptions/saga.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/mixins/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/mixins/saga_state.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/mixins/saga_step_histrory.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/mixins/types.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/builder.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/context.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/enums/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/notify.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/retry.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/domain/models/saga_snapshot.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/contracts.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/models.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/repository.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/retry.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/contracts.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/event.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/factory.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/models.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/repository.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/retry.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/serialization.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/setup.cfg +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/task.md +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/conftest.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/conftest.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/models.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_compensation_flow.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_context_persistence.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_inbox_flow.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_lifecycle_hooks.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_notification_flow.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_outbox_flow.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/test_repository.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/__init__.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_builder.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_inbox_extensibility.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_orchestrator_helpers.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_outbox_extensibility.py +0 -0
- {python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_retry.py +0 -0
|
@@ -43,6 +43,7 @@ saga_orchestrator/domain/models/retry.py
|
|
|
43
43
|
saga_orchestrator/domain/models/saga_snapshot.py
|
|
44
44
|
saga_orchestrator/domain/models/step.py
|
|
45
45
|
saga_orchestrator/domain/models/enums/__init__.py
|
|
46
|
+
saga_orchestrator/domain/models/enums/base_str_enum.py
|
|
46
47
|
saga_orchestrator/domain/models/enums/saga_status.py
|
|
47
48
|
saga_orchestrator/domain/models/enums/saga_step_phase.py
|
|
48
49
|
saga_orchestrator/domain/models/enums/saga_step_status.py
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/_version.py
RENAMED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/engine.py
RENAMED
|
@@ -444,43 +444,91 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
444
444
|
|
|
445
445
|
async with self._session_maker() as session:
|
|
446
446
|
async with session.begin():
|
|
447
|
-
|
|
447
|
+
sagas_running = await self._repository.due_running(
|
|
448
448
|
session, now=now, limit=limit
|
|
449
449
|
)
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
450
|
+
limit = max(0, limit - len(sagas_running))
|
|
451
|
+
|
|
452
|
+
sagas_suspended = await self._repository.due_suspended(
|
|
453
|
+
session, now=now, limit=limit
|
|
453
454
|
)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
limit = max(0, limit - len(sagas_suspended))
|
|
456
|
+
|
|
457
|
+
sagas_compensating = await self._repository.due_compensating(
|
|
458
|
+
session, now=now, limit=limit
|
|
457
459
|
)
|
|
458
|
-
|
|
460
|
+
limit = max(0, limit - len(sagas_compensating))
|
|
459
461
|
|
|
460
|
-
|
|
462
|
+
sagas_comp_suspended = (
|
|
461
463
|
await self._repository.due_compensating_suspended(
|
|
462
|
-
session, now=now, limit=
|
|
464
|
+
session, now=now, limit=limit
|
|
463
465
|
)
|
|
464
466
|
)
|
|
465
467
|
|
|
466
|
-
for saga in
|
|
467
|
-
saga
|
|
468
|
-
saga.step_execution_token = uuid.uuid4()
|
|
469
|
-
saga.deadline_at = now + self._execution_lease
|
|
468
|
+
for saga in sagas_running:
|
|
469
|
+
self._prepare_saga_for_resume(saga, SagaStatus.RUNNING, now)
|
|
470
470
|
ready_ids.append(saga.id)
|
|
471
471
|
|
|
472
|
-
for saga in
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
472
|
+
for saga in sagas_suspended:
|
|
473
|
+
if self._is_waiting_for_events(saga):
|
|
474
|
+
self._handle_saga_timeout(saga, now)
|
|
475
|
+
else:
|
|
476
|
+
self._prepare_saga_for_resume(saga, SagaStatus.RUNNING, now)
|
|
477
|
+
ready_ids.append(saga.id)
|
|
478
|
+
|
|
479
|
+
for saga in [*sagas_compensating, *sagas_comp_suspended]:
|
|
480
|
+
self._prepare_saga_for_resume(saga, SagaStatus.COMPENSATING, now)
|
|
476
481
|
compensation_ids.append(saga.id)
|
|
477
482
|
|
|
478
483
|
for saga_id in ready_ids:
|
|
479
484
|
await self._drive(saga_id)
|
|
480
485
|
for saga_id in compensation_ids:
|
|
481
486
|
await self._run_compensation(saga_id)
|
|
487
|
+
|
|
482
488
|
return len(ready_ids) + len(compensation_ids)
|
|
483
489
|
|
|
490
|
+
def _prepare_saga_for_resume(
|
|
491
|
+
self, saga, status: "SagaStatus", now: datetime
|
|
492
|
+
) -> None:
|
|
493
|
+
"""Устанавливает общие поля для продолжения работы саги."""
|
|
494
|
+
saga.status = status
|
|
495
|
+
saga.step_execution_token = uuid.uuid4()
|
|
496
|
+
saga.deadline_at = now + self._execution_lease
|
|
497
|
+
|
|
498
|
+
def _is_waiting_for_events(self, saga) -> bool:
|
|
499
|
+
"""Проверяет, ожидает ли сага внешних событий."""
|
|
500
|
+
context = saga.context
|
|
501
|
+
return bool(context.awaiting_event_type or context.awaiting_event_types)
|
|
502
|
+
|
|
503
|
+
def _handle_saga_timeout(self, saga, now: datetime) -> None:
|
|
504
|
+
"""логика обработки таймаута ожидания событий."""
|
|
505
|
+
context = saga.context
|
|
506
|
+
event_types = context.awaiting_event_types or context.awaiting_event_type
|
|
507
|
+
|
|
508
|
+
saga.status = SagaStatus.TIMEOUT
|
|
509
|
+
saga.last_error = f"Timed out waiting for event(s): {event_types}"
|
|
510
|
+
saga.deadline_at = None
|
|
511
|
+
saga.step_execution_token = uuid.uuid4()
|
|
512
|
+
|
|
513
|
+
definition = self._registry[context.saga_name]
|
|
514
|
+
step_def = definition.steps[saga.current_step_index]
|
|
515
|
+
|
|
516
|
+
saga.step_history.append(
|
|
517
|
+
self._history_model_class(
|
|
518
|
+
timestamp=now,
|
|
519
|
+
phase=SagaStepPhase.EXECUTE,
|
|
520
|
+
status=SagaStepStatus.TIMEOUT,
|
|
521
|
+
step_id=step_def.step_id,
|
|
522
|
+
step_name=type(step_def.step).__name__,
|
|
523
|
+
attempt=saga.retry_counter + 1,
|
|
524
|
+
token=saga.step_execution_token,
|
|
525
|
+
input={"_system": "timeout"},
|
|
526
|
+
output=None,
|
|
527
|
+
error=saga.last_error,
|
|
528
|
+
skipped=False,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
484
532
|
async def get_snapshot(self, saga_id: UUID) -> SagaSnapshot:
|
|
485
533
|
"""Return the snapshot view of one saga."""
|
|
486
534
|
async with self._session_maker() as session:
|
|
@@ -533,7 +581,11 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
533
581
|
async with self._session_maker() as session:
|
|
534
582
|
async with session.begin():
|
|
535
583
|
saga = await self._repository.get_for_update(session, saga_id)
|
|
536
|
-
if saga.status not in {
|
|
584
|
+
if saga.status not in {
|
|
585
|
+
SagaStatus.SUSPENDED,
|
|
586
|
+
SagaStatus.FAILED,
|
|
587
|
+
SagaStatus.TIMEOUT,
|
|
588
|
+
}:
|
|
537
589
|
raise SagaStateError(
|
|
538
590
|
f"Cannot retry step when saga status is {saga.status}"
|
|
539
591
|
)
|
|
@@ -595,9 +647,37 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
|
|
|
595
647
|
async with self._session_maker() as session:
|
|
596
648
|
async with session.begin():
|
|
597
649
|
saga = await self._repository.get_for_update(session, saga_id)
|
|
650
|
+
error_message = saga.last_error or "Aborted by admin"
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
saga_name = saga.context.saga_name
|
|
654
|
+
definition = self._registry[saga_name]
|
|
655
|
+
step_def = definition.steps[saga.current_step_index]
|
|
656
|
+
step_id = step_def.step_id
|
|
657
|
+
step_name = type(step_def.step).__name__
|
|
658
|
+
except (KeyError, IndexError):
|
|
659
|
+
step_id = "__unknown__"
|
|
660
|
+
step_name = "Unknown"
|
|
661
|
+
|
|
662
|
+
saga.step_history.append(
|
|
663
|
+
self._history_model_class(
|
|
664
|
+
timestamp=datetime.now(UTC),
|
|
665
|
+
phase=SagaStepPhase.EXECUTE,
|
|
666
|
+
status=SagaStepStatus.ERROR,
|
|
667
|
+
step_id=step_id,
|
|
668
|
+
step_name=step_name,
|
|
669
|
+
attempt=saga.retry_counter + 1,
|
|
670
|
+
token=saga.step_execution_token,
|
|
671
|
+
input={"_admin": "abort"},
|
|
672
|
+
output=None,
|
|
673
|
+
error=error_message,
|
|
674
|
+
skipped=True,
|
|
675
|
+
)
|
|
676
|
+
)
|
|
677
|
+
|
|
598
678
|
saga.status = SagaStatus.FAILED
|
|
599
679
|
saga.deadline_at = None
|
|
600
|
-
saga.last_error =
|
|
680
|
+
saga.last_error = error_message
|
|
601
681
|
saga.step_execution_token = uuid.uuid4()
|
|
602
682
|
|
|
603
683
|
async def skip_step(
|
|
@@ -139,7 +139,7 @@ class SagaRepository(Generic[ModelT]):
|
|
|
139
139
|
) -> list[ModelT]:
|
|
140
140
|
"""Return due saga rows for one status ordered by deadline."""
|
|
141
141
|
stmt = (
|
|
142
|
-
|
|
142
|
+
self._base_query()
|
|
143
143
|
.where(
|
|
144
144
|
self.model_class.status == status,
|
|
145
145
|
self.model_class.deadline_at.is_not(None),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import auto
|
|
2
|
+
|
|
3
|
+
from .base_str_enum import BaseStrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SagaStatus(BaseStrEnum):
|
|
7
|
+
RUNNING = auto()
|
|
8
|
+
SUSPENDED = auto()
|
|
9
|
+
FAILED = auto()
|
|
10
|
+
COMPENSATING = auto()
|
|
11
|
+
COMPLETED = auto()
|
|
12
|
+
COMPENSATING_SUSPENDED = auto()
|
|
13
|
+
COMPENSATED = auto()
|
|
14
|
+
TIMEOUT = auto()
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_terminal(self) -> bool:
|
|
18
|
+
return self in {self.FAILED, self.COMPLETED, self.COMPENSATED}
|
|
@@ -119,19 +119,24 @@ class BaseStep(Generic[InputModelT, OutputModelT]):
|
|
|
119
119
|
|
|
120
120
|
concrete_args = get_args(generic_base)
|
|
121
121
|
if not concrete_args or any(isinstance(arg, TypeVar) for arg in concrete_args):
|
|
122
|
-
raise TypeValidationError(
|
|
123
|
-
|
|
122
|
+
raise TypeValidationError(
|
|
123
|
+
f"Step '{cls.__name__}' inherits from a generic Step "
|
|
124
|
+
"but was not parameterized with concrete Input/Output models."
|
|
125
|
+
)
|
|
124
126
|
|
|
125
127
|
concrete_input_model, concrete_output_model = concrete_args
|
|
126
128
|
try:
|
|
127
|
-
hints = get_type_hints(
|
|
129
|
+
hints = get_type_hints(
|
|
130
|
+
cls.execute,
|
|
131
|
+
globalns=inspect.getmodule(cls).__dict__,
|
|
132
|
+
include_extras=True,
|
|
133
|
+
)
|
|
128
134
|
except (AttributeError, NameError) as e:
|
|
129
|
-
|
|
135
|
+
raise TypeValidationError(
|
|
130
136
|
f"Could not resolve type hints for '{cls.__name__}.execute'. "
|
|
131
137
|
f"Ensure all types are correctly imported. Original error: {e}"
|
|
132
138
|
)
|
|
133
139
|
|
|
134
|
-
|
|
135
140
|
if "inp" not in hints or "return" not in hints:
|
|
136
141
|
raise TypeValidationError(
|
|
137
142
|
f"Step '{cls.__name__}' must type annotate execute(inp) and return type"
|
|
@@ -144,7 +149,9 @@ class BaseStep(Generic[InputModelT, OutputModelT]):
|
|
|
144
149
|
input_model = input_annotation
|
|
145
150
|
|
|
146
151
|
return_annotation = hints["return"]
|
|
147
|
-
output_model = cls._resolve_output_model(
|
|
152
|
+
output_model = cls._resolve_output_model(
|
|
153
|
+
return_annotation, concrete_output_model
|
|
154
|
+
)
|
|
148
155
|
if not (inspect.isclass(input_model) and issubclass(input_model, BaseModel)):
|
|
149
156
|
raise TypeValidationError(
|
|
150
157
|
f"Step '{cls.__name__}' input must inherit from pydantic BaseModel"
|
|
@@ -153,7 +160,9 @@ class BaseStep(Generic[InputModelT, OutputModelT]):
|
|
|
153
160
|
cls.output_model = output_model
|
|
154
161
|
|
|
155
162
|
@staticmethod
|
|
156
|
-
def _resolve_output_model(
|
|
163
|
+
def _resolve_output_model(
|
|
164
|
+
annotation: Any, concrete_model: type[BaseModel] | None = None
|
|
165
|
+
) -> type[BaseModel]:
|
|
157
166
|
def find_model_in_args(args_tuple: tuple) -> list[type[BaseModel]]:
|
|
158
167
|
candidates = []
|
|
159
168
|
for arg in args_tuple:
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/helpers.py
RENAMED
|
@@ -324,7 +324,14 @@ class WaitingStep(BaseStep[NextInput, NextOutput]):
|
|
|
324
324
|
raise RuntimeError("pending approval")
|
|
325
325
|
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
class WaitingWithTimeoutStep(BaseStep[StartInput, StartOutput]):
|
|
328
|
+
"""Шаг, который всегда ждет события с небольшим таймаутом."""
|
|
329
|
+
|
|
330
|
+
async def execute(self, inp: StartInput) -> StepAwaitEvent | StartOutput:
|
|
331
|
+
return StepAwaitEvent(
|
|
332
|
+
event_types=("some.event",),
|
|
333
|
+
until=timedelta(milliseconds=10),
|
|
334
|
+
)
|
|
328
335
|
|
|
329
336
|
|
|
330
337
|
class SagaStartedEvent(BaseModel):
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# tests/integration/test_admin_api.py
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations
|
|
4
2
|
|
|
5
3
|
import asyncio
|
|
@@ -7,7 +5,7 @@ from datetime import timedelta
|
|
|
7
5
|
|
|
8
6
|
import pytest
|
|
9
7
|
|
|
10
|
-
from saga_orchestrator import SagaAdmin, SagaBuilder
|
|
8
|
+
from saga_orchestrator import SagaAdmin, SagaBuilder, SagaStepStatus
|
|
11
9
|
from saga_orchestrator.core.orchestrator import SagaOrchestrator
|
|
12
10
|
from saga_orchestrator.domain.exceptions import SagaStateError
|
|
13
11
|
from saga_orchestrator.domain.models import ExponentialRetry
|
|
@@ -23,6 +21,7 @@ from tests.integration.helpers import (
|
|
|
23
21
|
StartInput,
|
|
24
22
|
StartOutput,
|
|
25
23
|
WaitingStep,
|
|
24
|
+
WaitingWithTimeoutStep,
|
|
26
25
|
)
|
|
27
26
|
from tests.integration.models import IntegrationSagaHistory, IntegrationSagaState
|
|
28
27
|
|
|
@@ -58,7 +57,7 @@ async def test_admin_retry_rejects_failed_saga_after_compensation(session_maker)
|
|
|
58
57
|
)
|
|
59
58
|
state = await admin.get_saga(saga_id)
|
|
60
59
|
assert state.status == SagaStatus.COMPENSATED
|
|
61
|
-
assert any(entry.phase == "
|
|
60
|
+
assert any(entry.phase == "COMPENSATE" for entry in state.step_history)
|
|
62
61
|
|
|
63
62
|
with pytest.raises(SagaStateError):
|
|
64
63
|
await admin.retry_step(saga_id)
|
|
@@ -150,7 +149,7 @@ async def test_admin_compensate_rolls_back_suspended_saga(session_maker):
|
|
|
150
149
|
state_after = await admin.get_saga(saga_id)
|
|
151
150
|
assert state_after.status == SagaStatus.COMPENSATED
|
|
152
151
|
assert compensating_step.compensated is True
|
|
153
|
-
assert any(entry.phase == "
|
|
152
|
+
assert any(entry.phase == "COMPENSATE" for entry in state_after.step_history)
|
|
154
153
|
|
|
155
154
|
|
|
156
155
|
@pytest.mark.asyncio
|
|
@@ -225,7 +224,7 @@ async def test_get_admin_snapshot_returns(session_maker):
|
|
|
225
224
|
assert isinstance(admin_snapshot.step_history, list)
|
|
226
225
|
assert len(admin_snapshot.step_history) == 1
|
|
227
226
|
assert admin_snapshot.step_history[0].step_name == "AddOneStep"
|
|
228
|
-
assert admin_snapshot.step_history[0].status == "
|
|
227
|
+
assert admin_snapshot.step_history[0].status == "SUCCESS"
|
|
229
228
|
|
|
230
229
|
|
|
231
230
|
@pytest.mark.asyncio
|
|
@@ -307,3 +306,91 @@ async def test_can_retry_step_on_forward_failure_without_compensation(session_ma
|
|
|
307
306
|
state_after = await admin.get_saga(saga_id)
|
|
308
307
|
assert state_after.status == SagaStatus.COMPLETED
|
|
309
308
|
assert fails_once_step.calls == 2
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@pytest.mark.asyncio
|
|
312
|
+
async def test_admin_abort_adds_history_entry(session_maker):
|
|
313
|
+
"""
|
|
314
|
+
Проверяет, что вызов admin.abort() добавляет запись об ошибке в историю шага.
|
|
315
|
+
"""
|
|
316
|
+
builder = SagaBuilder()
|
|
317
|
+
builder.add_step(
|
|
318
|
+
step=WaitingWithTimeoutStep(),
|
|
319
|
+
input_map=lambda ctx: StartInput(value=ctx.initial_data["value"]),
|
|
320
|
+
retry_policy=ExponentialRetry(max_attempts=1, base_delay=timedelta(hours=1)),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
orchestrator = SagaOrchestrator[IntegrationSagaState, IntegrationSagaHistory](
|
|
324
|
+
model_class=IntegrationSagaState,
|
|
325
|
+
history_model_class=IntegrationSagaHistory,
|
|
326
|
+
session_maker=session_maker,
|
|
327
|
+
)
|
|
328
|
+
admin = SagaAdmin[IntegrationSagaState, IntegrationSagaHistory](
|
|
329
|
+
engine=orchestrator.engine
|
|
330
|
+
)
|
|
331
|
+
orchestrator.register("abort-test", builder.build())
|
|
332
|
+
|
|
333
|
+
saga_id = await orchestrator.start(
|
|
334
|
+
saga_name="abort-test",
|
|
335
|
+
initial_data={"value": 1},
|
|
336
|
+
aggregation_id="agg-abort-test",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
state_before = await admin.get_saga(saga_id)
|
|
340
|
+
assert state_before.status == SagaStatus.SUSPENDED
|
|
341
|
+
assert len(state_before.step_history) == 1
|
|
342
|
+
await admin.abort(saga_id)
|
|
343
|
+
|
|
344
|
+
state_after = await admin.get_saga(saga_id)
|
|
345
|
+
assert state_after.status == SagaStatus.FAILED
|
|
346
|
+
assert state_after.last_error == "Aborted by admin"
|
|
347
|
+
assert len(state_after.step_history) == 2
|
|
348
|
+
|
|
349
|
+
abort_entry = state_after.step_history[-1]
|
|
350
|
+
assert abort_entry.status == SagaStepStatus.ERROR
|
|
351
|
+
assert abort_entry.error == "Aborted by admin"
|
|
352
|
+
assert abort_entry.skipped is True
|
|
353
|
+
assert abort_entry.input == {"_admin": "abort"}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@pytest.mark.asyncio
|
|
357
|
+
async def test_admin_retry_step_from_timed_out_status(session_maker):
|
|
358
|
+
"""
|
|
359
|
+
Проверяет, что можно перезапустить шаг из статуса TIMEOUT.
|
|
360
|
+
"""
|
|
361
|
+
builder = SagaBuilder()
|
|
362
|
+
builder.add_step(
|
|
363
|
+
step=WaitingWithTimeoutStep(),
|
|
364
|
+
input_map=lambda ctx: StartInput(value=ctx.initial_data["value"]),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
orchestrator = SagaOrchestrator[IntegrationSagaState, IntegrationSagaHistory](
|
|
368
|
+
model_class=IntegrationSagaState,
|
|
369
|
+
history_model_class=IntegrationSagaHistory,
|
|
370
|
+
session_maker=session_maker,
|
|
371
|
+
)
|
|
372
|
+
admin = SagaAdmin[IntegrationSagaState, IntegrationSagaHistory](
|
|
373
|
+
engine=orchestrator.engine
|
|
374
|
+
)
|
|
375
|
+
orchestrator.register("retry-from-timeout", builder.build())
|
|
376
|
+
|
|
377
|
+
saga_id = await orchestrator.start(
|
|
378
|
+
saga_name="retry-from-timeout",
|
|
379
|
+
initial_data={"value": 1},
|
|
380
|
+
aggregation_id="agg-retry-from-timeout",
|
|
381
|
+
)
|
|
382
|
+
await asyncio.sleep(0.1)
|
|
383
|
+
await orchestrator.run_due()
|
|
384
|
+
|
|
385
|
+
state_before = await admin.get_saga(saga_id)
|
|
386
|
+
assert state_before.status == SagaStatus.TIMEOUT
|
|
387
|
+
assert len(state_before.step_history) == 2
|
|
388
|
+
assert state_before.step_history[0].status == SagaStepStatus.WAITING
|
|
389
|
+
assert state_before.step_history[1].status == SagaStepStatus.TIMEOUT
|
|
390
|
+
await admin.retry_step(saga_id)
|
|
391
|
+
state_after = await admin.get_saga(saga_id)
|
|
392
|
+
assert state_after.status == SagaStatus.SUSPENDED
|
|
393
|
+
assert state_after.retry_counter == 0
|
|
394
|
+
|
|
395
|
+
assert len(state_after.step_history) == 3
|
|
396
|
+
assert state_after.step_history[2].status == SagaStepStatus.WAITING
|
|
@@ -5,7 +5,7 @@ from datetime import timedelta
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from saga_orchestrator import SagaAdmin, SagaBuilder
|
|
8
|
+
from saga_orchestrator import SagaAdmin, SagaBuilder, SagaStepPhase, SagaStepStatus
|
|
9
9
|
from saga_orchestrator.core.orchestrator import SagaOrchestrator
|
|
10
10
|
from saga_orchestrator.domain.exceptions import ActiveSagaAlreadyExistsError
|
|
11
11
|
from saga_orchestrator.domain.models import ExponentialRetry
|
|
@@ -26,6 +26,7 @@ from tests.integration.helpers import (
|
|
|
26
26
|
ReserveQueueInput,
|
|
27
27
|
ReserveQueueStep,
|
|
28
28
|
StartInput,
|
|
29
|
+
WaitingWithTimeoutStep,
|
|
29
30
|
)
|
|
30
31
|
from tests.integration.models import (
|
|
31
32
|
IntegrationOutboxMessage,
|
|
@@ -398,3 +399,50 @@ async def test_get_snapshot_returns(session_maker):
|
|
|
398
399
|
assert snapshot.current_step_index == 1
|
|
399
400
|
assert snapshot.deadline_at is None
|
|
400
401
|
assert snapshot.last_error is None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@pytest.mark.asyncio
|
|
405
|
+
async def test_timed_out_status_on_await_event_deadline(session_maker):
|
|
406
|
+
"""
|
|
407
|
+
Проверяет, что сага переходит в статус TIMEOUT, если истекает время ожидания события.
|
|
408
|
+
"""
|
|
409
|
+
builder = SagaBuilder()
|
|
410
|
+
builder.add_step(
|
|
411
|
+
step=WaitingWithTimeoutStep(),
|
|
412
|
+
input_map=lambda ctx: StartInput(value=ctx.initial_data["value"]),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
orchestrator = SagaOrchestrator[IntegrationSagaState, IntegrationSagaHistory](
|
|
416
|
+
model_class=IntegrationSagaState,
|
|
417
|
+
history_model_class=IntegrationSagaHistory,
|
|
418
|
+
session_maker=session_maker,
|
|
419
|
+
)
|
|
420
|
+
admin = SagaAdmin[IntegrationSagaState, IntegrationSagaHistory](
|
|
421
|
+
engine=orchestrator.engine
|
|
422
|
+
)
|
|
423
|
+
orchestrator.register("await-timeout", builder.build())
|
|
424
|
+
saga_id = await orchestrator.start(
|
|
425
|
+
saga_name="await-timeout",
|
|
426
|
+
initial_data={"value": 1},
|
|
427
|
+
aggregation_id="agg-await-timeout",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
state_before = await admin.get_saga(saga_id)
|
|
431
|
+
assert state_before.status == SagaStatus.SUSPENDED
|
|
432
|
+
assert state_before.deadline_at is not None
|
|
433
|
+
|
|
434
|
+
await asyncio.sleep(0.1)
|
|
435
|
+
resumed = await orchestrator.run_due()
|
|
436
|
+
assert resumed == 0
|
|
437
|
+
state_after = await admin.get_saga(saga_id)
|
|
438
|
+
assert state_after.status == SagaStatus.TIMEOUT
|
|
439
|
+
assert "Timed out waiting for event" in state_after.last_error
|
|
440
|
+
assert state_after.deadline_at is None
|
|
441
|
+
|
|
442
|
+
assert len(state_after.step_history) == 2
|
|
443
|
+
assert state_after.step_history[0].status == SagaStepStatus.WAITING
|
|
444
|
+
assert state_after.step_history[0].phase == SagaStepPhase.EXECUTE
|
|
445
|
+
assert not state_after.step_history[0].error
|
|
446
|
+
assert state_after.step_history[1].status == SagaStepStatus.TIMEOUT
|
|
447
|
+
assert state_after.step_history[1].phase == SagaStepPhase.EXECUTE
|
|
448
|
+
assert "Timed out" in state_after.step_history[1].error
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_input_context.py
RENAMED
|
@@ -81,6 +81,6 @@ def test_input_context_properties_parametrized(
|
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
assert input_ctx.latest_event_type == expected_type, f"Failed on test: {test_id}"
|
|
84
|
-
assert (
|
|
85
|
-
|
|
86
|
-
)
|
|
84
|
+
assert input_ctx.latest_event_payload == expected_payload, (
|
|
85
|
+
f"Failed on test: {test_id}"
|
|
86
|
+
)
|
|
@@ -40,7 +40,6 @@ class StepAwaitEvent:
|
|
|
40
40
|
event_types: tuple[str, ...] | None = None
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
|
|
44
43
|
IntermediateInputT = TypeVar("IntermediateInputT", bound=BaseModel)
|
|
45
44
|
IntermediateOutputT = TypeVar("IntermediateOutputT", bound=BaseModel)
|
|
46
45
|
|
|
@@ -48,18 +47,19 @@ IntermediateOutputT = TypeVar("IntermediateOutputT", bound=BaseModel)
|
|
|
48
47
|
class AbstractIntermediateStep(
|
|
49
48
|
BaseStep[IntermediateInputT, IntermediateOutputT],
|
|
50
49
|
ABC,
|
|
51
|
-
Generic[IntermediateInputT, IntermediateOutputT]
|
|
50
|
+
Generic[IntermediateInputT, IntermediateOutputT],
|
|
52
51
|
):
|
|
53
52
|
@abstractmethod
|
|
54
53
|
def some_abstract_method(self):
|
|
55
54
|
pass
|
|
56
55
|
|
|
57
|
-
async def execute(
|
|
56
|
+
async def execute(
|
|
57
|
+
self, inp: IntermediateInputT
|
|
58
|
+
) -> IntermediateOutputT | StepAwaitEvent:
|
|
58
59
|
pass
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
class TestTypeResolution:
|
|
62
|
-
|
|
63
63
|
def test_indirect_inheritance_resolves_types_correctly(self):
|
|
64
64
|
"""
|
|
65
65
|
GIVEN a concrete step class inheriting from a generic abstract step
|
|
@@ -97,6 +97,7 @@ class TestTypeResolution:
|
|
|
97
97
|
THEN TypeValidationError should be raised.
|
|
98
98
|
"""
|
|
99
99
|
with pytest.raises(TypeValidationError, match="must type annotate execute"):
|
|
100
|
+
|
|
100
101
|
class FaultyStep(BaseStep[InputModelA, OutputModelA]):
|
|
101
102
|
# `inp` не аннотирован
|
|
102
103
|
async def execute(self, inp) -> OutputModelA:
|
|
@@ -114,6 +115,7 @@ class TestTypeResolution:
|
|
|
114
115
|
pass
|
|
115
116
|
|
|
116
117
|
with pytest.raises(TypeValidationError, match="Could not resolve type hints"):
|
|
118
|
+
|
|
117
119
|
class FaultyStep(BaseStep[InputModelA, OutputModelA]):
|
|
118
120
|
async def execute(self, inp: NotAModel) -> OutputModelA:
|
|
119
121
|
pass
|
|
@@ -124,7 +126,11 @@ class TestTypeResolution:
|
|
|
124
126
|
WHEN the class is defined
|
|
125
127
|
THEN TypeValidationError should be raised by our custom logic.
|
|
126
128
|
"""
|
|
127
|
-
with pytest.raises(
|
|
129
|
+
with pytest.raises(
|
|
130
|
+
TypeValidationError,
|
|
131
|
+
match="inherits from a generic Step but was not parameterized with concrete Input/Output models",
|
|
132
|
+
):
|
|
133
|
+
|
|
128
134
|
class UnparameterizedStep(AbstractIntermediateStep):
|
|
129
135
|
def some_abstract_method(self):
|
|
130
136
|
pass
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from enum import StrEnum
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class SagaStatus(StrEnum):
|
|
5
|
-
RUNNING = "RUNNING"
|
|
6
|
-
SUSPENDED = "SUSPENDED"
|
|
7
|
-
FAILED = "FAILED"
|
|
8
|
-
COMPENSATING = "COMPENSATING"
|
|
9
|
-
COMPLETED = "COMPLETED"
|
|
10
|
-
COMPENSATING_SUSPENDED = "COMPENSATING_SUSPENDED"
|
|
11
|
-
COMPENSATED = "COMPENSATED"
|
|
12
|
-
|
|
13
|
-
@property
|
|
14
|
-
def is_terminal(self) -> bool:
|
|
15
|
-
return self in {self.FAILED, self.COMPLETED, self.COMPENSATED}
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/compensation_flow.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/http_and_queue.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/examples/retry_recovery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/admin/api.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/__init__.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/core/builder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/inbox/retry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/event.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/saga_orchestrator/outbox/retry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/__init__.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/conftest.py
RENAMED
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/integration/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_saga_orchestrator-0.2.4 → python_saga_orchestrator-0.4.0}/tests/unit/test_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|