Lightweight embedded saga orchestrator for asyncio Python services
Project description
python-saga-orchestrator
Lightweight embedded saga orchestration for asyncio Python services.
The library implements the Saga pattern for long-running business processes that:
- span multiple steps,
- call external systems,
- need retry and compensation,
- must survive worker crashes and process restarts.
Unlike external workflow platforms, this library runs inside your service and stores saga state in your application's database through SQLAlchemy.
What it provides
- typed step definitions with
Pydanticmodels - saga construction with
SagaBuilderandStepRef - persisted saga state through
SagaStateMixin - runtime execution through
SagaOrchestratorandSagaEngine - retry, timeout, recovery, and compensation
- administrative operations through
SagaAdmin - PostgreSQL-first reliability using
SELECT ... FOR UPDATE
Installation
Requirements:
- Python 3.12+
- PostgreSQL for production-grade execution semantics
Install from PyPI:
pip install python-saga-orchestrator
Or with uv:
uv pip install python-saga-orchestrator
Install from the repository source:
pip install .
For local development:
pip install '.[dev]'
Core concepts
BaseStep
Each saga step is a class with:
execute(inp) -> out- optional
compensate(inp, out) -> None
Steps are regular Python objects. In practice they are created once at application startup and reused.
SagaBuilder
SagaBuilder creates an immutable SagaDefinition.
Each added step includes:
- the step object
input_map- optional timeout
- retry policy
- optional dependency on a previous step via
StepRef
SagaStateMixin
Your SQLAlchemy model inherits SagaStateMixin to store:
- current status
- current step index
- execution token
- context
- step history
- deadline
- retry counter
SagaOrchestrator
SagaOrchestrator is the public runtime API used by application code:
register(...)start(...)notify(...)run_due(...)get_snapshot(...)
SagaAdmin
SagaAdmin exposes operational controls:
get_saga(...)retry_step(...)skip_step(...)compensate_step(...)abort(...)
Quick start
from datetime import timedelta
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from saga_orchestrator import (
BaseStep,
ExponentialRetry,
SagaAdmin,
SagaBuilder,
SagaOrchestrator,
SagaStateMixin,
)
class Base(DeclarativeBase):
pass
class OrderSagaState(Base, SagaStateMixin):
__tablename__ = "order_saga_state"
class ReserveInput(BaseModel):
order_id: str
class ReserveOutput(BaseModel):
reservation_id: str
class ChargeInput(BaseModel):
reservation_id: str
class ChargeOutput(BaseModel):
payment_id: str
class ReserveInventoryStep(BaseStep[ReserveInput, ReserveOutput]):
async def execute(self, inp: ReserveInput) -> ReserveOutput:
return ReserveOutput(reservation_id=f"res-{inp.order_id}")
async def compensate(self, inp: ReserveInput, out: ReserveOutput) -> None:
return None
class ChargePaymentStep(BaseStep[ChargeInput, ChargeOutput]):
async def execute(self, inp: ChargeInput) -> ChargeOutput:
return ChargeOutput(payment_id=f"pay-{inp.reservation_id}")
def build_order_saga():
builder = SagaBuilder()
reserve_ref = builder.add_step(
step=ReserveInventoryStep(),
input_map=lambda ctx: ReserveInput(order_id=ctx.initial_data["order_id"]),
)
builder.add_step(
step=ChargePaymentStep(),
depends_on=reserve_ref,
input_map=lambda out: ChargeInput(reservation_id=out.reservation_id),
retry_policy=ExponentialRetry(
max_attempts=3,
base_delay=timedelta(seconds=5),
),
)
return builder.build()
def setup_saga(
session_maker: async_sessionmaker,
) -> tuple[SagaOrchestrator[OrderSagaState], SagaAdmin[OrderSagaState]]:
orchestrator = SagaOrchestrator[OrderSagaState](
model_class=OrderSagaState,
session_maker=session_maker,
)
orchestrator.register("create_order_v1", build_order_saga())
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
return orchestrator, admin
Start a saga:
orchestrator, admin = setup_saga(session_maker)
saga_id = await orchestrator.start(
saga_name="create_order_v1",
initial_data={"order_id": "order-123"},
aggregation_id="order-123",
)
Recovery model
The library persists enough state to recover work after failures:
RUNNINGsagas with expired execution leases can be reclaimedSUSPENDEDsagas with expired retry deadlines can be resumedCOMPENSATINGsagas can continue rollback after a crash
The recovery entry point is:
await orchestrator.run_due(limit=100)
In production this should be called by a background worker or scheduled job.
Notifications and external events
Use notify(...) when a suspended saga should resume because of an external signal:
accepted = await orchestrator.notify(
saga_id=saga_id,
token=current_token,
event={"approved": True},
)
Configure explicit event expectations through a public API:
token = await orchestrator.await_event(
saga_id=saga_id,
event=AwaitingEvent(
event_type="model.approved",
correlation_id="corr-123",
),
)
The event payload is stored in saga context and can be used by root-step input_map functions through InputContext.
Administrative operations
Get the full persisted state:
snapshot = await admin.get_saga(saga_id)
print(snapshot.status)
print(snapshot.step_history)
Retry the current failed step:
await admin.retry_step(saga_id)
Skip the current suspended step:
await admin.skip_step(
saga_id,
mock_output={"payment_id": "manual-payment"},
)
Start compensation manually:
await admin.compensate_step(saga_id)
Abort the saga:
await admin.abort(saga_id)
Persistence expectations
The library is PostgreSQL-first.
Important implementation details:
- state transitions are performed inside database transactions
- mutating reads use
SELECT ... FOR UPDATE - JSON state is stored in
contextandstep_history step_execution_tokenis used to reject stale events and stale step completions
SQLite may be sufficient for local experiments, but PostgreSQL should be used for integration testing and production use.
Example workflow
A runnable end-to-end example is available in:
examples/llm_deploy.pyexamples/retry_recovery.pyexamples/compensation_flow.pyexamples/admin_skip.py
These examples demonstrate:
- basic model deployment
- retry and recovery through
run_due() - compensation after failure
- admin-driven step skipping
Running tests
Run unit tests:
pytest -q tests/unit
Run PostgreSQL integration tests:
export TEST_DATABASE_URL='postgresql+asyncpg://postgres:postgres@localhost:5432/saga_test_db'
pytest -q tests/integration
The integration fixture creates an isolated schema per test, so it does not require a dedicated empty database schema.
Current limitations
- the implementation is optimized for sequential saga execution, not parallel DAG execution
- PostgreSQL is the intended reliability target
- tracing integration is not implemented yet
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_saga_orchestrator-0.1.3.tar.gz.
File metadata
- Download URL: python_saga_orchestrator-0.1.3.tar.gz
- Upload date:
- Size: 22.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b7101bee465f54b410841f2eeaceae23ab24271c69bbc1652fd59ff6fd118ac
|
|
| MD5 |
443254e8244565e83ca548c49d7713bb
|
|
| BLAKE2b-256 |
37a30f54c65a5de4b57373f3e28944988460952df88c01e56a8ded995a7a590a
|
Provenance
The following attestation bundles were made for python_saga_orchestrator-0.1.3.tar.gz:
Publisher:
publish.yml on mdvasilyev/python-saga-orchestrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_saga_orchestrator-0.1.3.tar.gz -
Subject digest:
5b7101bee465f54b410841f2eeaceae23ab24271c69bbc1652fd59ff6fd118ac - Sigstore transparency entry: 1384426872
- Sigstore integration time:
-
Permalink:
mdvasilyev/python-saga-orchestrator@7f1254cf31f8c8d0c6a3e1e16106dcd19ba92a65 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/mdvasilyev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7f1254cf31f8c8d0c6a3e1e16106dcd19ba92a65 -
Trigger Event:
push
-
Statement type:
File details
Details for the file python_saga_orchestrator-0.1.3-py3-none-any.whl.
File metadata
- Download URL: python_saga_orchestrator-0.1.3-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff54ec600ea754ad36edff92426487ada5405b58022808b2cf0f05cf5084014e
|
|
| MD5 |
5ee6cd120d7646892788244b0497ea18
|
|
| BLAKE2b-256 |
742ac57e7fcbe86c99cbf032b273f9717eeb30707e6cab8856fb65f7f86060ad
|
Provenance
The following attestation bundles were made for python_saga_orchestrator-0.1.3-py3-none-any.whl:
Publisher:
publish.yml on mdvasilyev/python-saga-orchestrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_saga_orchestrator-0.1.3-py3-none-any.whl -
Subject digest:
ff54ec600ea754ad36edff92426487ada5405b58022808b2cf0f05cf5084014e - Sigstore transparency entry: 1384426951
- Sigstore integration time:
-
Permalink:
mdvasilyev/python-saga-orchestrator@7f1254cf31f8c8d0c6a3e1e16106dcd19ba92a65 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/mdvasilyev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7f1254cf31f8c8d0c6a3e1e16106dcd19ba92a65 -
Trigger Event:
push
-
Statement type: