python-saga-orchestrator 0.1.4__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.4/python_saga_orchestrator.egg-info → python_saga_orchestrator-0.2.3.dev0}/PKG-INFO +29 -6
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/README.md +28 -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.4 → python_saga_orchestrator-0.2.3.dev0}/pyproject.toml +14 -2
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0/python_saga_orchestrator.egg-info}/PKG-INFO +29 -6
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/SOURCES.txt +41 -1
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/__init__.py +5 -2
- python_saga_orchestrator-0.2.3.dev0/saga_orchestrator/_version.py +24 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/admin/api.py +4 -2
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/builder.py +40 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/engine.py +347 -176
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/orchestrator.py +5 -1
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/repository.py +24 -27
- {python_saga_orchestrator-0.1.4 → 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.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/__init__.py +4 -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.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/saga_snapshot.py +5 -3
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/step.py +24 -2
- {python_saga_orchestrator-0.1.4 → 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.4/saga_orchestrator/domain/mixins/saga_state.py +0 -57
- python_saga_orchestrator-0.1.4/saga_orchestrator/domain/models/builder.py +0 -12
- python_saga_orchestrator-0.1.4/saga_orchestrator/domain/models/enums/__init__.py +0 -7
- python_saga_orchestrator-0.1.4/saga_orchestrator/domain/models/enums/saga_status.py +0 -13
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/LICENSE +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/dependency_links.txt +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/requires.txt +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/python_saga_orchestrator.egg-info/top_level.txt +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/admin/__init__.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/core/__init__.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/__init__.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/exceptions/__init__.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/exceptions/saga.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/notify.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/domain/models/retry.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/inbox/__init__.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/inbox/contracts.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/inbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/inbox/models.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/inbox/repository.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/inbox/retry.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/__init__.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/contracts.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/dispatcher.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/event.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/models.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/repository.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/retry.py +0 -0
- {python_saga_orchestrator-0.1.4 → python_saga_orchestrator-0.2.3.dev0}/saga_orchestrator/outbox/serialization.py +0 -0
- {python_saga_orchestrator-0.1.4 → 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
|
|
@@ -137,11 +137,13 @@ Your SQLAlchemy model inherits `SagaStateMixin` to store:
|
|
|
137
137
|
## Quick start
|
|
138
138
|
|
|
139
139
|
```python
|
|
140
|
+
import uuid
|
|
140
141
|
from datetime import timedelta
|
|
141
142
|
|
|
142
143
|
from pydantic import BaseModel
|
|
144
|
+
from sqlalchemy import ForeignKey
|
|
143
145
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
144
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
146
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
145
147
|
|
|
146
148
|
from saga_orchestrator import (
|
|
147
149
|
BaseStep,
|
|
@@ -150,6 +152,7 @@ from saga_orchestrator import (
|
|
|
150
152
|
SagaBuilder,
|
|
151
153
|
SagaOrchestrator,
|
|
152
154
|
SagaStateMixin,
|
|
155
|
+
SagaStepHistoryMixin,
|
|
153
156
|
)
|
|
154
157
|
|
|
155
158
|
|
|
@@ -157,9 +160,24 @@ class Base(DeclarativeBase):
|
|
|
157
160
|
pass
|
|
158
161
|
|
|
159
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
|
+
|
|
160
172
|
class OrderSagaState(Base, SagaStateMixin):
|
|
161
173
|
__tablename__ = "order_saga_state"
|
|
162
174
|
|
|
175
|
+
step_history: Mapped[list[OrderSagaHistory]] = relationship(
|
|
176
|
+
"OrderSagaHistory",
|
|
177
|
+
cascade="all, delete-orphan",
|
|
178
|
+
order_by="OrderSagaHistory.id",
|
|
179
|
+
)
|
|
180
|
+
|
|
163
181
|
|
|
164
182
|
class ReserveInput(BaseModel):
|
|
165
183
|
order_id: str
|
|
@@ -212,15 +230,20 @@ def build_order_saga():
|
|
|
212
230
|
|
|
213
231
|
|
|
214
232
|
def setup_saga(
|
|
215
|
-
|
|
216
|
-
) -> tuple[
|
|
217
|
-
|
|
233
|
+
session_maker: async_sessionmaker,
|
|
234
|
+
) -> tuple[
|
|
235
|
+
SagaOrchestrator[OrderSagaState, OrderSagaHistory],
|
|
236
|
+
SagaAdmin[OrderSagaState, OrderSagaHistory]
|
|
237
|
+
]:
|
|
238
|
+
orchestrator = SagaOrchestrator[OrderSagaState, OrderSagaHistory](
|
|
218
239
|
model_class=OrderSagaState,
|
|
240
|
+
history_model_class=OrderSagaHistory,
|
|
219
241
|
session_maker=session_maker,
|
|
220
242
|
)
|
|
221
243
|
orchestrator.register("create_order_v1", build_order_saga())
|
|
222
244
|
|
|
223
|
-
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
|
|
245
|
+
admin = SagaAdmin[OrderSagaState, OrderSagaHistory](engine=orchestrator.engine)
|
|
246
|
+
|
|
224
247
|
return orchestrator, admin
|
|
225
248
|
```
|
|
226
249
|
|
|
@@ -103,11 +103,13 @@ Your SQLAlchemy model inherits `SagaStateMixin` to store:
|
|
|
103
103
|
## Quick start
|
|
104
104
|
|
|
105
105
|
```python
|
|
106
|
+
import uuid
|
|
106
107
|
from datetime import timedelta
|
|
107
108
|
|
|
108
109
|
from pydantic import BaseModel
|
|
110
|
+
from sqlalchemy import ForeignKey
|
|
109
111
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
110
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
112
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
111
113
|
|
|
112
114
|
from saga_orchestrator import (
|
|
113
115
|
BaseStep,
|
|
@@ -116,6 +118,7 @@ from saga_orchestrator import (
|
|
|
116
118
|
SagaBuilder,
|
|
117
119
|
SagaOrchestrator,
|
|
118
120
|
SagaStateMixin,
|
|
121
|
+
SagaStepHistoryMixin,
|
|
119
122
|
)
|
|
120
123
|
|
|
121
124
|
|
|
@@ -123,9 +126,24 @@ class Base(DeclarativeBase):
|
|
|
123
126
|
pass
|
|
124
127
|
|
|
125
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
|
+
|
|
126
138
|
class OrderSagaState(Base, SagaStateMixin):
|
|
127
139
|
__tablename__ = "order_saga_state"
|
|
128
140
|
|
|
141
|
+
step_history: Mapped[list[OrderSagaHistory]] = relationship(
|
|
142
|
+
"OrderSagaHistory",
|
|
143
|
+
cascade="all, delete-orphan",
|
|
144
|
+
order_by="OrderSagaHistory.id",
|
|
145
|
+
)
|
|
146
|
+
|
|
129
147
|
|
|
130
148
|
class ReserveInput(BaseModel):
|
|
131
149
|
order_id: str
|
|
@@ -178,15 +196,20 @@ def build_order_saga():
|
|
|
178
196
|
|
|
179
197
|
|
|
180
198
|
def setup_saga(
|
|
181
|
-
|
|
182
|
-
) -> tuple[
|
|
183
|
-
|
|
199
|
+
session_maker: async_sessionmaker,
|
|
200
|
+
) -> tuple[
|
|
201
|
+
SagaOrchestrator[OrderSagaState, OrderSagaHistory],
|
|
202
|
+
SagaAdmin[OrderSagaState, OrderSagaHistory]
|
|
203
|
+
]:
|
|
204
|
+
orchestrator = SagaOrchestrator[OrderSagaState, OrderSagaHistory](
|
|
184
205
|
model_class=OrderSagaState,
|
|
206
|
+
history_model_class=OrderSagaHistory,
|
|
185
207
|
session_maker=session_maker,
|
|
186
208
|
)
|
|
187
209
|
orchestrator.register("create_order_v1", build_order_saga())
|
|
188
210
|
|
|
189
|
-
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
|
|
211
|
+
admin = SagaAdmin[OrderSagaState, OrderSagaHistory](engine=orchestrator.engine)
|
|
212
|
+
|
|
190
213
|
return orchestrator, admin
|
|
191
214
|
```
|
|
192
215
|
|
|
@@ -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())
|