python-saga-orchestrator 0.7.0__tar.gz → 0.7.1__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.
Files changed (89) hide show
  1. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/Makefile +1 -1
  2. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/PKG-INFO +1 -1
  3. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/python_saga_orchestrator.egg-info/PKG-INFO +1 -1
  4. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/_version.py +2 -2
  5. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/core/engine.py +1 -0
  6. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/helpers.py +60 -0
  7. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_core_flow.py +63 -0
  8. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/.github/workflows/ci.yml +0 -0
  9. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/.github/workflows/publish.yml +0 -0
  10. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/.gitignore +0 -0
  11. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/Dockerfile +0 -0
  12. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/LICENSE +0 -0
  13. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/README.md +0 -0
  14. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/docker-compose.yaml +0 -0
  15. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/examples/admin_skip.py +0 -0
  16. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/examples/common.py +0 -0
  17. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/examples/compensation_flow.py +0 -0
  18. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/examples/http_and_queue.py +0 -0
  19. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/examples/llm_deploy.py +0 -0
  20. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/examples/retry_recovery.py +0 -0
  21. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/pyproject.toml +0 -0
  22. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/python_saga_orchestrator.egg-info/SOURCES.txt +0 -0
  23. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/python_saga_orchestrator.egg-info/dependency_links.txt +0 -0
  24. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/python_saga_orchestrator.egg-info/requires.txt +0 -0
  25. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/python_saga_orchestrator.egg-info/top_level.txt +0 -0
  26. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/__init__.py +0 -0
  27. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/admin/__init__.py +0 -0
  28. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/admin/api.py +0 -0
  29. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/core/__init__.py +0 -0
  30. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/core/builder.py +0 -0
  31. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/core/orchestrator.py +0 -0
  32. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/core/repository.py +0 -0
  33. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/__init__.py +0 -0
  34. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/exceptions/__init__.py +0 -0
  35. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/exceptions/saga.py +0 -0
  36. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/mixins/__init__.py +0 -0
  37. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/mixins/saga_state.py +0 -0
  38. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/mixins/saga_step_histrory.py +0 -0
  39. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/mixins/types.py +0 -0
  40. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/__init__.py +0 -0
  41. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/builder.py +0 -0
  42. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/context.py +0 -0
  43. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/enums/__init__.py +0 -0
  44. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/enums/base_str_enum.py +0 -0
  45. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/enums/saga_status.py +0 -0
  46. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/enums/saga_step_phase.py +0 -0
  47. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/enums/saga_step_status.py +0 -0
  48. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/notify.py +0 -0
  49. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/retry.py +0 -0
  50. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/saga_snapshot.py +0 -0
  51. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/domain/models/step.py +0 -0
  52. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/inbox/__init__.py +0 -0
  53. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/inbox/contracts.py +0 -0
  54. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/inbox/dispatcher.py +0 -0
  55. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/inbox/models.py +0 -0
  56. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/inbox/repository.py +0 -0
  57. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/inbox/retry.py +0 -0
  58. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/__init__.py +0 -0
  59. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/contracts.py +0 -0
  60. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/dispatcher.py +0 -0
  61. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/event.py +0 -0
  62. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/factory.py +0 -0
  63. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/models.py +0 -0
  64. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/repository.py +0 -0
  65. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/retry.py +0 -0
  66. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/saga_orchestrator/outbox/serialization.py +0 -0
  67. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/setup.cfg +0 -0
  68. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/task.md +0 -0
  69. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/__init__.py +0 -0
  70. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/conftest.py +0 -0
  71. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/__init__.py +0 -0
  72. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/conftest.py +0 -0
  73. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/models.py +0 -0
  74. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_admin_api.py +0 -0
  75. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_compensation_flow.py +0 -0
  76. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_context_persistence.py +0 -0
  77. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_inbox_flow.py +0 -0
  78. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_lifecycle_hooks.py +0 -0
  79. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_notification_flow.py +0 -0
  80. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_outbox_flow.py +0 -0
  81. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/integration/test_repository.py +0 -0
  82. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/__init__.py +0 -0
  83. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_builder.py +0 -0
  84. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_inbox_extensibility.py +0 -0
  85. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_input_context.py +0 -0
  86. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_orchestrator_helpers.py +0 -0
  87. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_outbox_extensibility.py +0 -0
  88. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_retry.py +0 -0
  89. {python_saga_orchestrator-0.7.0 → python_saga_orchestrator-0.7.1}/tests/unit/test_step_type_resolution.py +0 -0
@@ -13,4 +13,4 @@ tests:
13
13
  --build \
14
14
  --abort-on-container-exit \
15
15
  --exit-code-from tests
16
- docker compose down -v
16
+ docker compose down -v --rmi local
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-saga-orchestrator
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Lightweight embedded saga orchestrator for asyncio Python services
5
5
  Author-email: Maxim Vasilyev <mayxis@inbox.ru>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-saga-orchestrator
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Lightweight embedded saga orchestrator for asyncio Python services
5
5
  Author-email: Maxim Vasilyev <mayxis@inbox.ru>
6
6
  License-Expression: MIT
@@ -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.7.0'
22
- __version_tuple__ = version_tuple = (0, 7, 0)
21
+ __version__ = version = '0.7.1'
22
+ __version_tuple__ = version_tuple = (0, 7, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1046,6 +1046,7 @@ class SagaEngine(Generic[ModelT, HistoryModelT]):
1046
1046
  )
1047
1047
 
1048
1048
  saga.last_error = repr(error)
1049
+ context.clear_latest_event()
1049
1050
  next_attempt = saga.retry_counter + 1
1050
1051
  delay = step_def.retry_policy.next_delay(next_attempt)
1051
1052
  saga.retry_counter = next_attempt
@@ -471,6 +471,66 @@ class RetryWaitStep(BaseStep[RetryWaitInput, StartOutput]):
471
471
  return StartOutput(value=inp.value + 1)
472
472
 
473
473
 
474
+ class LeakTrackInput(BaseModel):
475
+ value: str
476
+
477
+
478
+ class LeakTrackOutput(BaseModel):
479
+ value: str
480
+
481
+
482
+ class CompensateEventTrackerStep(BaseStep[LeakTrackInput, LeakTrackOutput]):
483
+ """Шаг 1: Успешно выполняется, а при компенсации запоминает, какой ивент ему прислали."""
484
+
485
+ def __init__(self) -> None:
486
+ self.compensate_event_type: str | None = "NOT_CALLED"
487
+
488
+ async def execute(
489
+ self,
490
+ inp: LeakTrackInput,
491
+ event_type: str | None = None,
492
+ event_payload: Any | None = None,
493
+ ) -> LeakTrackOutput:
494
+ return LeakTrackOutput(value=inp.value)
495
+
496
+ async def compensate(
497
+ self,
498
+ inp: LeakTrackInput,
499
+ out: LeakTrackOutput,
500
+ event_type: str | None = None,
501
+ event_payload: Any | None = None,
502
+ ) -> None:
503
+ self.compensate_event_type = event_type
504
+
505
+
506
+ class AsyncFailInput(BaseModel):
507
+ value: str
508
+
509
+
510
+ class AsyncFailOutput(BaseModel):
511
+ value: str
512
+
513
+
514
+ class AsyncFailStep(BaseStep[AsyncFailInput, AsyncFailOutput]):
515
+ """Шаг 2: Засыпает, а при получении события - кидает ошибку."""
516
+
517
+ async def execute(
518
+ self,
519
+ inp: AsyncFailInput,
520
+ event_type: str | None = None,
521
+ event_payload: Any | None = None,
522
+ ) -> AsyncFailOutput | StepAwaitEvent:
523
+ if event_type is None:
524
+ return StepAwaitEvent(
525
+ event_types=("fail.event",),
526
+ correlation_id=inp.value,
527
+ )
528
+ if event_type == "fail.event":
529
+ raise RuntimeError("Step failed due to incoming event")
530
+
531
+ return AsyncFailOutput(value=inp.value)
532
+
533
+
474
534
  class SagaStartedEvent(BaseModel):
475
535
  saga_id: uuid.UUID
476
536
  initial_value: int
@@ -23,11 +23,15 @@ from tests.integration.helpers import (
23
23
  ActivateQueueStep,
24
24
  AddOneStep,
25
25
  AlwaysFailStep,
26
+ AsyncFailInput,
27
+ AsyncFailStep,
28
+ CompensateEventTrackerStep,
26
29
  CompensatingStep,
27
30
  FailsOnceStep,
28
31
  FlakyStep,
29
32
  HttpInput,
30
33
  HttpStep,
34
+ LeakTrackInput,
31
35
  NextInput,
32
36
  RecoverableStep,
33
37
  ReserveQueueInput,
@@ -560,3 +564,62 @@ async def test_retry_after_timeout_processes_successfully(session_maker):
560
564
 
561
565
  final_state = await admin.get_saga(saga_id)
562
566
  assert final_state.status == SagaStatus.COMPLETED
567
+
568
+
569
+ @pytest.mark.asyncio
570
+ async def test_event_does_not_leak_into_compensation_after_failure(session_maker):
571
+ """
572
+ Проверяет, что если шаг упал после пробуждения от события,
573
+ это событие очищается и не 'протекает' в компенсацию предыдущих шагов.
574
+ """
575
+ tracker_step = CompensateEventTrackerStep()
576
+ fail_step = AsyncFailStep()
577
+
578
+ builder = SagaBuilder()
579
+ step1_ref = builder.add_step(
580
+ step=tracker_step,
581
+ input_map=lambda ctx: LeakTrackInput(value=ctx.initial_data["correlation"]),
582
+ )
583
+ builder.add_step(
584
+ step=fail_step,
585
+ depends_on=step1_ref,
586
+ input_map=lambda out: AsyncFailInput(value=out.value),
587
+ )
588
+
589
+ orchestrator = SagaOrchestrator[IntegrationSagaState, IntegrationSagaHistory](
590
+ model_class=IntegrationSagaState,
591
+ history_model_class=IntegrationSagaHistory,
592
+ session_maker=session_maker,
593
+ )
594
+ admin = SagaAdmin[IntegrationSagaState, IntegrationSagaHistory](
595
+ engine=orchestrator.engine
596
+ )
597
+ orchestrator.register("leak_test_saga", builder.build())
598
+
599
+ saga_id = await orchestrator.start(
600
+ saga_name="leak_test_saga",
601
+ initial_data={"correlation": "corr-leak-123"},
602
+ aggregation_id="agg-leak-test",
603
+ )
604
+
605
+ state1 = await admin.get_saga(saga_id)
606
+ assert state1.status == SagaStatus.SUSPENDED
607
+ assert state1.current_step_index == 1
608
+
609
+ await orchestrator.notify(
610
+ saga_id=saga_id,
611
+ token=state1.step_execution_token,
612
+ event=NotifyEvent(
613
+ event_id="evt-leak",
614
+ event_type="fail.event",
615
+ correlation_id="corr-leak-123",
616
+ payload={"reason": "fatal error"},
617
+ ),
618
+ )
619
+
620
+ final_state = await admin.get_saga(saga_id)
621
+ assert final_state.status == SagaStatus.COMPENSATED
622
+
623
+ assert tracker_step.compensate_event_type is None, (
624
+ f"Event leaked into compensation! Expected None, got: {tracker_step.compensate_event_type}"
625
+ )