python-saga-orchestrator 0.1.3__tar.gz → 0.2.3.dev0__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.3.dev0/.github/workflows/ci.yml +55 -0
- python_saga_orchestrator-0.2.3.dev0/.github/workflows/publish.yml +52 -0
- python_saga_orchestrator-0.2.3.dev0/.gitignore +207 -0
- python_saga_orchestrator-0.2.3.dev0/Dockerfile +18 -0
- python_saga_orchestrator-0.2.3.dev0/Makefile +12 -0
- {python_saga_orchestrator-0.1.3/python_saga_orchestrator.egg-info → python_saga_orchestrator-0.2.3.dev0}/PKG-INFO +48 -6
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/README.md +47 -5
- python_saga_orchestrator-0.2.3.dev0/docker-compose.yaml +29 -0
- python_saga_orchestrator-0.2.3.dev0/examples/admin_skip.py +55 -0
- python_saga_orchestrator-0.2.3.dev0/examples/common.py +161 -0
- python_saga_orchestrator-0.2.3.dev0/examples/compensation_flow.py +51 -0
- python_saga_orchestrator-0.2.3.dev0/examples/http_and_queue.py +413 -0
- python_saga_orchestrator-0.2.3.dev0/examples/llm_deploy.py +114 -0
- python_saga_orchestrator-0.2.3.dev0/examples/retry_recovery.py +62 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/pyproject.toml +14 -2
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0/python_saga_orchestrator.egg-info}/PKG-INFO +48 -6
- python_saga_orchestrator-0.2.3.dev0/python_saga_orchestrator.egg-info/SOURCES.txt +84 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/__init__.py +33 -2
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/_version.py +24 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/admin/api.py +4 -2
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/builder.py +40 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/engine.py +575 -145
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/orchestrator.py +40 -1
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/repository.py +24 -27
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/mixins/__init__.py +2 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/mixins/saga_state.py +62 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/mixins/saga_step_histrory.py +45 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/mixins/types.py +78 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/__init__.py +6 -1
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/models/builder.py +23 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/models/context.py +126 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/models/enums/__init__.py +11 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/models/enums/saga_status.py +15 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/models/enums/saga_step_phase.py +7 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/domain/models/enums/saga_step_status.py +7 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/notify.py +1 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/saga_snapshot.py +5 -3
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/step.py +66 -9
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/inbox/__init__.py +27 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/inbox/contracts.py +81 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/inbox/dispatcher.py +120 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/inbox/models.py +84 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/inbox/repository.py +165 -0
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/inbox/retry.py +20 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/factory.py +4 -4
- python_saga_orchestrator-0.2.3.dev0/task.md +245 -0
- python_saga_orchestrator-0.2.3.dev0/tests/__init__.py +0 -0
- python_saga_orchestrator-0.2.3.dev0/tests/conftest.py +6 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/__init__.py +0 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/conftest.py +49 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/helpers.py +398 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/models.py +40 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_admin_api.py +309 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_compensation_flow.py +351 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_context_persistence.py +290 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_core_flow.py +400 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_inbox_flow.py +73 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_lifecycle_hooks.py +209 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_notification_flow.py +257 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_outbox_flow.py +183 -0
- python_saga_orchestrator-0.2.3.dev0/tests/integration/test_repository.py +218 -0
- python_saga_orchestrator-0.2.3.dev0/tests/unit/__init__.py +0 -0
- python_saga_orchestrator-0.2.3.dev0/tests/unit/test_builder.py +149 -0
- python_saga_orchestrator-0.2.3.dev0/tests/unit/test_inbox_extensibility.py +165 -0
- python_saga_orchestrator-0.2.3.dev0/tests/unit/test_orchestrator_helpers.py +81 -0
- python_saga_orchestrator-0.2.3.dev0/tests/unit/test_outbox_extensibility.py +183 -0
- python_saga_orchestrator-0.2.3.dev0/tests/unit/test_retry.py +40 -0
- python_saga_orchestrator-0.1.3/python_saga_orchestrator.egg-info/SOURCES.txt +0 -38
- python_saga_orchestrator-0.1.3/saga_orchestrator/domain/mixins/saga_state.py +0 -57
- python_saga_orchestrator-0.1.3/saga_orchestrator/domain/models/builder.py +0 -12
- python_saga_orchestrator-0.1.3/saga_orchestrator/domain/models/enums/__init__.py +0 -7
- python_saga_orchestrator-0.1.3/saga_orchestrator/domain/models/enums/saga_status.py +0 -13
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/LICENSE +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/dependency_links.txt +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/requires.txt +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/top_level.txt +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/admin/__init__.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/__init__.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/__init__.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/exceptions/__init__.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/exceptions/saga.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/retry.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/__init__.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/contracts.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/event.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/models.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/repository.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/retry.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/serialization.py +0 -0
- {python_saga_orchestrator-0.1.3 → python_saga_orchestrator-0.2.3.dev0}/setup.cfg +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test-and-build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
postgres:
|
|
15
|
+
image: postgres:16
|
|
16
|
+
env:
|
|
17
|
+
POSTGRES_DB: saga_test_db
|
|
18
|
+
POSTGRES_USER: postgres
|
|
19
|
+
POSTGRES_PASSWORD: postgres
|
|
20
|
+
ports:
|
|
21
|
+
- 5432:5432
|
|
22
|
+
options: >-
|
|
23
|
+
--health-cmd "pg_isready -U postgres -d saga_test_db"
|
|
24
|
+
--health-interval 10s
|
|
25
|
+
--health-timeout 5s
|
|
26
|
+
--health-retries 5
|
|
27
|
+
|
|
28
|
+
env:
|
|
29
|
+
TEST_DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/saga_test_db
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- name: Check out repository
|
|
33
|
+
uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- name: Set up Python
|
|
36
|
+
uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: |
|
|
42
|
+
python -m pip install --upgrade pip
|
|
43
|
+
python -m pip install .[dev]
|
|
44
|
+
|
|
45
|
+
- name: Lint
|
|
46
|
+
run: ruff check .
|
|
47
|
+
|
|
48
|
+
- name: Run tests
|
|
49
|
+
run: pytest -q
|
|
50
|
+
|
|
51
|
+
- name: Build package
|
|
52
|
+
run: python -m build
|
|
53
|
+
|
|
54
|
+
- name: Validate distributions
|
|
55
|
+
run: python -m twine check dist/*
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
environment:
|
|
16
|
+
name: pypi
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Check out repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
|
|
29
|
+
- name: Install build tools
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install --upgrade pip
|
|
32
|
+
python -m pip install build twine setuptools-scm
|
|
33
|
+
|
|
34
|
+
- name: Validate version matches tag
|
|
35
|
+
run: |
|
|
36
|
+
EXPECTED_VERSION="${GITHUB_REF_NAME#v}"
|
|
37
|
+
COMPUTED_VERSION="$(python -m setuptools_scm)"
|
|
38
|
+
echo "Tag version: ${EXPECTED_VERSION}"
|
|
39
|
+
echo "Computed version: ${COMPUTED_VERSION}"
|
|
40
|
+
if [ "${COMPUTED_VERSION}" != "${EXPECTED_VERSION}" ]; then
|
|
41
|
+
echo "Version mismatch: tag ${EXPECTED_VERSION}, computed ${COMPUTED_VERSION}"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
- name: Build package
|
|
46
|
+
run: python -m build
|
|
47
|
+
|
|
48
|
+
- name: Validate distributions
|
|
49
|
+
run: python -m twine check dist/*
|
|
50
|
+
|
|
51
|
+
- name: Publish to PyPI
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.envrc
|
|
140
|
+
.venv
|
|
141
|
+
env/
|
|
142
|
+
venv/
|
|
143
|
+
ENV/
|
|
144
|
+
env.bak/
|
|
145
|
+
venv.bak/
|
|
146
|
+
|
|
147
|
+
# Spyder project settings
|
|
148
|
+
.spyderproject
|
|
149
|
+
.spyproject
|
|
150
|
+
|
|
151
|
+
# Rope project settings
|
|
152
|
+
.ropeproject
|
|
153
|
+
|
|
154
|
+
# mkdocs documentation
|
|
155
|
+
/site
|
|
156
|
+
|
|
157
|
+
# mypy
|
|
158
|
+
.mypy_cache/
|
|
159
|
+
.dmypy.json
|
|
160
|
+
dmypy.json
|
|
161
|
+
|
|
162
|
+
# Pyre type checker
|
|
163
|
+
.pyre/
|
|
164
|
+
|
|
165
|
+
# pytype static type analyzer
|
|
166
|
+
.pytype/
|
|
167
|
+
|
|
168
|
+
# Cython debug symbols
|
|
169
|
+
cython_debug/
|
|
170
|
+
|
|
171
|
+
# PyCharm
|
|
172
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
173
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
174
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
175
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
176
|
+
.idea/
|
|
177
|
+
|
|
178
|
+
# Abstra
|
|
179
|
+
# Abstra is an AI-powered process automation framework.
|
|
180
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
181
|
+
# Learn more at https://abstra.io/docs
|
|
182
|
+
.abstra/
|
|
183
|
+
|
|
184
|
+
# Visual Studio Code
|
|
185
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
186
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
188
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
189
|
+
# .vscode/
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
# PyPI configuration file
|
|
195
|
+
.pypirc
|
|
196
|
+
|
|
197
|
+
# Cursor
|
|
198
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
199
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
200
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
201
|
+
.cursorignore
|
|
202
|
+
.cursorindexingignore
|
|
203
|
+
|
|
204
|
+
# Marimo
|
|
205
|
+
marimo/_static/
|
|
206
|
+
marimo/_lsp/
|
|
207
|
+
__marimo__/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM ghcr.io/astral-sh/uv:python3.13-alpine3.23
|
|
2
|
+
|
|
3
|
+
WORKDIR /app/
|
|
4
|
+
|
|
5
|
+
ENV PIP_NO_CACHE_DIR=1 \
|
|
6
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
7
|
+
PYTHONUNBUFFERED=1 \
|
|
8
|
+
UV_NO_DEV=0 \
|
|
9
|
+
PATH="/app/.venv/bin:$PATH" \
|
|
10
|
+
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTHON_SAGA_ORCHESTRATOR="0.0.1.dev"
|
|
11
|
+
|
|
12
|
+
COPY pyproject.toml pyproject.toml
|
|
13
|
+
|
|
14
|
+
RUN uv sync
|
|
15
|
+
|
|
16
|
+
COPY saga_orchestrator saga_orchestrator
|
|
17
|
+
COPY tests tests
|
|
18
|
+
CMD ["pytest"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-saga-orchestrator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.3.dev0
|
|
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
|
|
@@ -51,6 +51,7 @@ Unlike external workflow platforms, this library runs inside your service and st
|
|
|
51
51
|
- persisted saga state through `SagaStateMixin`
|
|
52
52
|
- runtime execution through `SagaOrchestrator` and `SagaEngine`
|
|
53
53
|
- retry, timeout, recovery, and compensation
|
|
54
|
+
- async queue-style steps through `StepAwaitEvent` and `notify(...)`
|
|
54
55
|
- administrative operations through `SagaAdmin`
|
|
55
56
|
- PostgreSQL-first reliability using `SELECT ... FOR UPDATE`
|
|
56
57
|
|
|
@@ -136,11 +137,13 @@ Your SQLAlchemy model inherits `SagaStateMixin` to store:
|
|
|
136
137
|
## Quick start
|
|
137
138
|
|
|
138
139
|
```python
|
|
140
|
+
import uuid
|
|
139
141
|
from datetime import timedelta
|
|
140
142
|
|
|
141
143
|
from pydantic import BaseModel
|
|
144
|
+
from sqlalchemy import ForeignKey
|
|
142
145
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
143
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
146
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
144
147
|
|
|
145
148
|
from saga_orchestrator import (
|
|
146
149
|
BaseStep,
|
|
@@ -149,6 +152,7 @@ from saga_orchestrator import (
|
|
|
149
152
|
SagaBuilder,
|
|
150
153
|
SagaOrchestrator,
|
|
151
154
|
SagaStateMixin,
|
|
155
|
+
SagaStepHistoryMixin,
|
|
152
156
|
)
|
|
153
157
|
|
|
154
158
|
|
|
@@ -156,9 +160,24 @@ class Base(DeclarativeBase):
|
|
|
156
160
|
pass
|
|
157
161
|
|
|
158
162
|
|
|
163
|
+
class OrderSagaHistory(Base, SagaStepHistoryMixin):
|
|
164
|
+
__tablename__ = "order_saga_history"
|
|
165
|
+
|
|
166
|
+
saga_id: Mapped[uuid.UUID] = mapped_column(
|
|
167
|
+
ForeignKey("order_saga_state.id", ondelete="CASCADE"),
|
|
168
|
+
index=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
159
172
|
class OrderSagaState(Base, SagaStateMixin):
|
|
160
173
|
__tablename__ = "order_saga_state"
|
|
161
174
|
|
|
175
|
+
step_history: Mapped[list[OrderSagaHistory]] = relationship(
|
|
176
|
+
"OrderSagaHistory",
|
|
177
|
+
cascade="all, delete-orphan",
|
|
178
|
+
order_by="OrderSagaHistory.id",
|
|
179
|
+
)
|
|
180
|
+
|
|
162
181
|
|
|
163
182
|
class ReserveInput(BaseModel):
|
|
164
183
|
order_id: str
|
|
@@ -211,15 +230,20 @@ def build_order_saga():
|
|
|
211
230
|
|
|
212
231
|
|
|
213
232
|
def setup_saga(
|
|
214
|
-
|
|
215
|
-
) -> tuple[
|
|
216
|
-
|
|
233
|
+
session_maker: async_sessionmaker,
|
|
234
|
+
) -> tuple[
|
|
235
|
+
SagaOrchestrator[OrderSagaState, OrderSagaHistory],
|
|
236
|
+
SagaAdmin[OrderSagaState, OrderSagaHistory]
|
|
237
|
+
]:
|
|
238
|
+
orchestrator = SagaOrchestrator[OrderSagaState, OrderSagaHistory](
|
|
217
239
|
model_class=OrderSagaState,
|
|
240
|
+
history_model_class=OrderSagaHistory,
|
|
218
241
|
session_maker=session_maker,
|
|
219
242
|
)
|
|
220
243
|
orchestrator.register("create_order_v1", build_order_saga())
|
|
221
244
|
|
|
222
|
-
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
|
|
245
|
+
admin = SagaAdmin[OrderSagaState, OrderSagaHistory](engine=orchestrator.engine)
|
|
246
|
+
|
|
223
247
|
return orchestrator, admin
|
|
224
248
|
```
|
|
225
249
|
|
|
@@ -277,6 +301,23 @@ token = await orchestrator.await_event(
|
|
|
277
301
|
|
|
278
302
|
The event payload is stored in saga context and can be used by root-step `input_map` functions through `InputContext`.
|
|
279
303
|
|
|
304
|
+
For distributed consumers, use transactional inbox ingestion first, then process inbox rows:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
stored = await orchestrator.ingest_event(
|
|
308
|
+
aggregation_id="order-123",
|
|
309
|
+
event={
|
|
310
|
+
"event_id": "evt-123",
|
|
311
|
+
"event_type": "payment.completed",
|
|
312
|
+
"correlation_id": "corr-123",
|
|
313
|
+
"payload": {"payment_id": "pay-1"},
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if stored:
|
|
318
|
+
await orchestrator.run_inbox_due(limit=100)
|
|
319
|
+
```
|
|
320
|
+
|
|
280
321
|
## Administrative operations
|
|
281
322
|
|
|
282
323
|
Get the full persisted state:
|
|
@@ -334,6 +375,7 @@ A runnable end-to-end example is available in:
|
|
|
334
375
|
- [`examples/retry_recovery.py`](./examples/retry_recovery.py)
|
|
335
376
|
- [`examples/compensation_flow.py`](./examples/compensation_flow.py)
|
|
336
377
|
- [`examples/admin_skip.py`](./examples/admin_skip.py)
|
|
378
|
+
- [`examples/http_and_queue.py`](./examples/http_and_queue.py)
|
|
337
379
|
|
|
338
380
|
These examples demonstrate:
|
|
339
381
|
- basic model deployment
|
|
@@ -17,6 +17,7 @@ Unlike external workflow platforms, this library runs inside your service and st
|
|
|
17
17
|
- persisted saga state through `SagaStateMixin`
|
|
18
18
|
- runtime execution through `SagaOrchestrator` and `SagaEngine`
|
|
19
19
|
- retry, timeout, recovery, and compensation
|
|
20
|
+
- async queue-style steps through `StepAwaitEvent` and `notify(...)`
|
|
20
21
|
- administrative operations through `SagaAdmin`
|
|
21
22
|
- PostgreSQL-first reliability using `SELECT ... FOR UPDATE`
|
|
22
23
|
|
|
@@ -102,11 +103,13 @@ Your SQLAlchemy model inherits `SagaStateMixin` to store:
|
|
|
102
103
|
## Quick start
|
|
103
104
|
|
|
104
105
|
```python
|
|
106
|
+
import uuid
|
|
105
107
|
from datetime import timedelta
|
|
106
108
|
|
|
107
109
|
from pydantic import BaseModel
|
|
110
|
+
from sqlalchemy import ForeignKey
|
|
108
111
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
109
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
112
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
110
113
|
|
|
111
114
|
from saga_orchestrator import (
|
|
112
115
|
BaseStep,
|
|
@@ -115,6 +118,7 @@ from saga_orchestrator import (
|
|
|
115
118
|
SagaBuilder,
|
|
116
119
|
SagaOrchestrator,
|
|
117
120
|
SagaStateMixin,
|
|
121
|
+
SagaStepHistoryMixin,
|
|
118
122
|
)
|
|
119
123
|
|
|
120
124
|
|
|
@@ -122,9 +126,24 @@ class Base(DeclarativeBase):
|
|
|
122
126
|
pass
|
|
123
127
|
|
|
124
128
|
|
|
129
|
+
class OrderSagaHistory(Base, SagaStepHistoryMixin):
|
|
130
|
+
__tablename__ = "order_saga_history"
|
|
131
|
+
|
|
132
|
+
saga_id: Mapped[uuid.UUID] = mapped_column(
|
|
133
|
+
ForeignKey("order_saga_state.id", ondelete="CASCADE"),
|
|
134
|
+
index=True,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
125
138
|
class OrderSagaState(Base, SagaStateMixin):
|
|
126
139
|
__tablename__ = "order_saga_state"
|
|
127
140
|
|
|
141
|
+
step_history: Mapped[list[OrderSagaHistory]] = relationship(
|
|
142
|
+
"OrderSagaHistory",
|
|
143
|
+
cascade="all, delete-orphan",
|
|
144
|
+
order_by="OrderSagaHistory.id",
|
|
145
|
+
)
|
|
146
|
+
|
|
128
147
|
|
|
129
148
|
class ReserveInput(BaseModel):
|
|
130
149
|
order_id: str
|
|
@@ -177,15 +196,20 @@ def build_order_saga():
|
|
|
177
196
|
|
|
178
197
|
|
|
179
198
|
def setup_saga(
|
|
180
|
-
|
|
181
|
-
) -> tuple[
|
|
182
|
-
|
|
199
|
+
session_maker: async_sessionmaker,
|
|
200
|
+
) -> tuple[
|
|
201
|
+
SagaOrchestrator[OrderSagaState, OrderSagaHistory],
|
|
202
|
+
SagaAdmin[OrderSagaState, OrderSagaHistory]
|
|
203
|
+
]:
|
|
204
|
+
orchestrator = SagaOrchestrator[OrderSagaState, OrderSagaHistory](
|
|
183
205
|
model_class=OrderSagaState,
|
|
206
|
+
history_model_class=OrderSagaHistory,
|
|
184
207
|
session_maker=session_maker,
|
|
185
208
|
)
|
|
186
209
|
orchestrator.register("create_order_v1", build_order_saga())
|
|
187
210
|
|
|
188
|
-
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
|
|
211
|
+
admin = SagaAdmin[OrderSagaState, OrderSagaHistory](engine=orchestrator.engine)
|
|
212
|
+
|
|
189
213
|
return orchestrator, admin
|
|
190
214
|
```
|
|
191
215
|
|
|
@@ -243,6 +267,23 @@ token = await orchestrator.await_event(
|
|
|
243
267
|
|
|
244
268
|
The event payload is stored in saga context and can be used by root-step `input_map` functions through `InputContext`.
|
|
245
269
|
|
|
270
|
+
For distributed consumers, use transactional inbox ingestion first, then process inbox rows:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
stored = await orchestrator.ingest_event(
|
|
274
|
+
aggregation_id="order-123",
|
|
275
|
+
event={
|
|
276
|
+
"event_id": "evt-123",
|
|
277
|
+
"event_type": "payment.completed",
|
|
278
|
+
"correlation_id": "corr-123",
|
|
279
|
+
"payload": {"payment_id": "pay-1"},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if stored:
|
|
284
|
+
await orchestrator.run_inbox_due(limit=100)
|
|
285
|
+
```
|
|
286
|
+
|
|
246
287
|
## Administrative operations
|
|
247
288
|
|
|
248
289
|
Get the full persisted state:
|
|
@@ -300,6 +341,7 @@ A runnable end-to-end example is available in:
|
|
|
300
341
|
- [`examples/retry_recovery.py`](./examples/retry_recovery.py)
|
|
301
342
|
- [`examples/compensation_flow.py`](./examples/compensation_flow.py)
|
|
302
343
|
- [`examples/admin_skip.py`](./examples/admin_skip.py)
|
|
344
|
+
- [`examples/http_and_queue.py`](./examples/http_and_queue.py)
|
|
303
345
|
|
|
304
346
|
These examples demonstrate:
|
|
305
347
|
- basic model deployment
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres:16-alpine3.19
|
|
4
|
+
container_name: saga-test-db
|
|
5
|
+
environment:
|
|
6
|
+
- POSTGRES_USER=testuser
|
|
7
|
+
- POSTGRES_PASSWORD=testpass
|
|
8
|
+
- POSTGRES_DB=testdb
|
|
9
|
+
healthcheck:
|
|
10
|
+
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
|
|
11
|
+
interval: 5s
|
|
12
|
+
timeout: 5s
|
|
13
|
+
retries: 5
|
|
14
|
+
|
|
15
|
+
tests:
|
|
16
|
+
build:
|
|
17
|
+
context: .
|
|
18
|
+
dockerfile: Dockerfile
|
|
19
|
+
container_name: saga-tests-runner
|
|
20
|
+
depends_on:
|
|
21
|
+
db:
|
|
22
|
+
condition: service_healthy
|
|
23
|
+
environment:
|
|
24
|
+
- TEST_DATABASE_URL=postgresql+asyncpg://testuser:testpass@db:5432/testdb
|
|
25
|
+
command: ["pytest", "-v", "-s"]
|
|
26
|
+
|
|
27
|
+
networks:
|
|
28
|
+
default:
|
|
29
|
+
driver: bridge
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
from examples.common import (
|
|
7
|
+
DeployOutput,
|
|
8
|
+
FinalizeInput,
|
|
9
|
+
FinalizeStep,
|
|
10
|
+
ManualApprovalStep,
|
|
11
|
+
StartInput,
|
|
12
|
+
create_runtime,
|
|
13
|
+
dispose_runtime,
|
|
14
|
+
print_snapshot,
|
|
15
|
+
)
|
|
16
|
+
from saga_orchestrator import ExponentialRetry, SagaBuilder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def main() -> None:
|
|
20
|
+
session_maker, orchestrator, admin = await create_runtime()
|
|
21
|
+
try:
|
|
22
|
+
builder = SagaBuilder()
|
|
23
|
+
approval_ref = builder.add_step(
|
|
24
|
+
step=ManualApprovalStep(),
|
|
25
|
+
input_map=lambda ctx: StartInput(model_name=ctx.initial_data["model_name"]),
|
|
26
|
+
retry_policy=ExponentialRetry(
|
|
27
|
+
max_attempts=1,
|
|
28
|
+
base_delay=timedelta(hours=1),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
builder.add_step(
|
|
32
|
+
step=FinalizeStep(),
|
|
33
|
+
depends_on=approval_ref,
|
|
34
|
+
input_map=lambda out: FinalizeInput(endpoint=out.endpoint),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
orchestrator.register("admin_skip_demo", builder.build())
|
|
38
|
+
saga_id = await orchestrator.start(
|
|
39
|
+
saga_name="admin_skip_demo",
|
|
40
|
+
initial_data={"model_name": "qwen-2.5"},
|
|
41
|
+
aggregation_id="admin-skip-demo",
|
|
42
|
+
)
|
|
43
|
+
await print_snapshot(admin, saga_id, title="Before admin skip")
|
|
44
|
+
|
|
45
|
+
await admin.skip_step(
|
|
46
|
+
saga_id,
|
|
47
|
+
mock_output=DeployOutput(endpoint="https://models.local/qwen-2.5"),
|
|
48
|
+
)
|
|
49
|
+
await print_snapshot(admin, saga_id, title="After admin skip")
|
|
50
|
+
finally:
|
|
51
|
+
await dispose_runtime(session_maker)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
asyncio.run(main())
|