sqlalchemy-state-machine 1.11.0__tar.gz → 2.0.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.
Files changed (37) hide show
  1. sqlalchemy_state_machine-2.0.0/.github/SECURITY.md +8 -0
  2. sqlalchemy_state_machine-2.0.0/.github/dependabot.yml +10 -0
  3. sqlalchemy_state_machine-2.0.0/.github/workflows/ci.yml +36 -0
  4. sqlalchemy_state_machine-2.0.0/.github/workflows/publish.yml +57 -0
  5. sqlalchemy_state_machine-2.0.0/.gitignore +10 -0
  6. sqlalchemy_state_machine-2.0.0/CHANGELOG.md +189 -0
  7. sqlalchemy_state_machine-2.0.0/Makefile +134 -0
  8. sqlalchemy_state_machine-2.0.0/PKG-INFO +159 -0
  9. sqlalchemy_state_machine-2.0.0/README.md +134 -0
  10. sqlalchemy_state_machine-2.0.0/codecov.yml +20 -0
  11. sqlalchemy_state_machine-2.0.0/examples/basic_usage.py +51 -0
  12. sqlalchemy_state_machine-2.0.0/pyproject.toml +45 -0
  13. sqlalchemy_state_machine-2.0.0/src/sqlalchemy_state_machine/__init__.py +16 -0
  14. sqlalchemy_state_machine-2.0.0/src/sqlalchemy_state_machine/exceptions.py +9 -0
  15. sqlalchemy_state_machine-2.0.0/src/sqlalchemy_state_machine/machine.py +93 -0
  16. sqlalchemy_state_machine-2.0.0/src/sqlalchemy_state_machine/mixin.py +90 -0
  17. sqlalchemy_state_machine-2.0.0/tests/test_examples.py +6 -0
  18. sqlalchemy_state_machine-2.0.0/tests/test_machine.py +62 -0
  19. sqlalchemy_state_machine-2.0.0/tests/test_mixin.py +216 -0
  20. sqlalchemy_state_machine-2.0.0/tests/test_public_api.py +13 -0
  21. sqlalchemy_state_machine-2.0.0/uv.lock +597 -0
  22. sqlalchemy-state-machine-1.11.0/MANIFEST.in +0 -6
  23. sqlalchemy-state-machine-1.11.0/PKG-INFO +0 -99
  24. sqlalchemy-state-machine-1.11.0/README.md +0 -69
  25. sqlalchemy-state-machine-1.11.0/pyproject.toml +0 -18
  26. sqlalchemy-state-machine-1.11.0/setup.cfg +0 -4
  27. sqlalchemy-state-machine-1.11.0/setup.py +0 -53
  28. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine/__init__.py +0 -6
  29. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine/state_mixin.py +0 -41
  30. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine/version.py +0 -3
  31. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine.egg-info/PKG-INFO +0 -99
  32. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine.egg-info/SOURCES.txt +0 -14
  33. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine.egg-info/dependency_links.txt +0 -1
  34. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine.egg-info/not-zip-safe +0 -1
  35. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine.egg-info/requires.txt +0 -2
  36. sqlalchemy-state-machine-1.11.0/sqlalchemy_state_machine.egg-info/top_level.txt +0 -2
  37. {sqlalchemy-state-machine-1.11.0 → sqlalchemy_state_machine-2.0.0}/LICENSE +0 -0
@@ -0,0 +1,8 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ We will endeavour to support:
6
+ * the most recent minor release with bug fixes
7
+ * the latest minor release from the last major version
8
+ for 6 months after a new major version is released with critical bug fixes
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: uv
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ quality:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.14"
17
+ - uses: astral-sh/setup-uv@v7
18
+ - run: uv sync --all-groups --locked
19
+ - run: uv run --locked ruff format --check src tests examples
20
+ - run: uv run --locked ruff check src tests examples
21
+ - run: uv run --locked mypy src
22
+
23
+ test:
24
+ runs-on: ubuntu-latest
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+ - uses: astral-sh/setup-uv@v7
35
+ - run: uv sync --all-groups --locked
36
+ - run: uv run --locked pytest --cov=sqlalchemy_state_machine --cov-report=term-missing
@@ -0,0 +1,57 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*", "[0-9]*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.14"
15
+ - uses: astral-sh/setup-uv@v7
16
+ - run: uv sync --all-groups --locked
17
+ - name: Verify tag matches project version
18
+ run: |
19
+ uv run --locked python - <<'PY'
20
+ import os
21
+ import tomllib
22
+
23
+ with open("pyproject.toml", "rb") as pyproject:
24
+ version = tomllib.load(pyproject)["project"]["version"]
25
+
26
+ tag = os.environ["GITHUB_REF_NAME"]
27
+ if tag.removeprefix("v") != version:
28
+ raise SystemExit(
29
+ f"Release tag {tag!r} does not match [project].version "
30
+ f"{version!r} in pyproject.toml."
31
+ )
32
+ PY
33
+ - run: uv run --locked ruff format --check src tests examples
34
+ - run: uv run --locked ruff check src tests examples
35
+ - run: uv run --locked mypy src
36
+ - run: uv run --locked pytest --cov=sqlalchemy_state_machine --cov-report=term-missing
37
+ - run: uv build
38
+ - run: uv run --locked python -m zipfile -t dist/*.whl
39
+ - uses: actions/upload-artifact@v4
40
+ with:
41
+ name: package-distributions
42
+ path: dist/
43
+
44
+ publish:
45
+ needs: build
46
+ runs-on: ubuntu-latest
47
+ environment:
48
+ name: pypi
49
+ url: https://pypi.org/p/sqlalchemy-state-machine
50
+ permissions:
51
+ id-token: write
52
+ steps:
53
+ - uses: actions/download-artifact@v4
54
+ with:
55
+ name: package-distributions
56
+ path: dist/
57
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .coverage
5
+ .mypy_cache/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ build/
9
+ dist/
10
+ *.egg-info/
@@ -0,0 +1,189 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0 (2026-07-20)
4
+
5
+ - **Breaking:** Python 3.11+ and SQLAlchemy 2.x rewrite.
6
+ - **Breaking:** Replace the `transitions` integration with explicit `StateMachine`, `Transition`, and `StateMachineMixin` declarations.
7
+ - Add typed validation, callbacks, transition availability inspection, modern packaging, tests, and CI.
8
+ - Remove `StateConfig`, dynamic trigger methods, manual event registration, and legacy Python support.
9
+
10
+ ## 1.11.0 (2024-10-18)
11
+
12
+ * Update pipeline [Pavel Liashkov]
13
+ * Update version [Pavel Liashkov]
14
+
15
+
16
+ ## 1.10.0 (2023-08-03)
17
+
18
+ * Update sqlalchemy requirement from <1.5,>=1.4 to >=1.4,<2.1 [dependabot[bot]]
19
+ * Update pipeline [Pavel Liashkov]
20
+ * Update version [Pavel Liashkov]
21
+
22
+
23
+ ## 1.9.0 (2023-01-19)
24
+
25
+ * Update transitions requirement from <0.9,>=0.8 to >=0.8,<0.10 [dependabot[bot]]
26
+ * Bump version [Pavel Liashkov]
27
+
28
+
29
+ ## 1.8.0 (2021-07-26)
30
+
31
+ * Relax reqs version [Pavel Liashkov]
32
+
33
+
34
+ ## 1.7.0 (2021-07-23)
35
+
36
+ * Bump sqlalchemy from 1.4.18 to 1.4.19 [dependabot[bot]]
37
+ * Bump sqlalchemy from 1.4.19 to 1.4.22 [dependabot[bot]]
38
+ * Update version.py [Pavel Liashkov]
39
+
40
+
41
+ ## 1.6.0 (2021-06-18)
42
+
43
+ * Bump sqlalchemy from 1.4.17 to 1.4.18 [dependabot[bot]]
44
+ * Update version.py [Pavel Liashkov]
45
+
46
+
47
+ ## 1.5.0 (2021-06-04)
48
+
49
+ * Bump sqlalchemy from 1.4.15 to 1.4.17 [dependabot[bot]]
50
+ * Update version.py [Pavel Liashkov]
51
+
52
+
53
+ ## 1.4.0 (2021-05-18)
54
+
55
+ * Bump sqlalchemy from 1.4.13 to 1.4.15 [dependabot[bot]]
56
+ * Update version.py [Pavel Liashkov]
57
+
58
+
59
+ ## 1.3.0 (2021-05-05)
60
+
61
+ * Bump sqlalchemy from 1.4.8 to 1.4.13 [dependabot[bot]]
62
+ * Update version.py [Pavel Liashkov]
63
+
64
+
65
+ ## 1.2.0 (2021-04-16)
66
+
67
+ * Bump transitions from 0.8.7 to 0.8.8 [dependabot[bot]]
68
+ * Bump sqlalchemy from 1.4.5 to 1.4.8 [dependabot[bot]]
69
+ * Update version.py [Pavel Liashkov]
70
+
71
+
72
+ ## 1.1.0 (2021-04-05)
73
+
74
+ * Bump sqlalchemy from 1.4.0 to 1.4.2 [dependabot[bot]]
75
+ * Bump sqlalchemy from 1.4.2 to 1.4.5 [dependabot[bot]]
76
+ * Bump version [Pavel Liashkov]
77
+
78
+
79
+ ## 1.0.0 (2021-03-17)
80
+
81
+ * Add command for check code quality [Pavel Liashkov]
82
+ * Fix githib workflow [Pavel Liashkov]
83
+ * Bump sqlalchemy from 1.3.23 to 1.4.0 [dependabot[bot]]
84
+ * NOTASK: Bump version [Pavel Liashkov]
85
+ * Bump version [Pavel Liashkov]
86
+
87
+
88
+ ## 0.5.5 (2021-02-20)
89
+
90
+ * Bump version [Pavel Liashkov]
91
+ * Update Makefile [Pavel Liashkov]
92
+ * Add codecov workflow [Pavel Liashkov]
93
+ * Add codecov badge [Pavel Liashkov]
94
+ * Bump transitions from 0.8.6 to 0.8.7 [dependabot[bot]]
95
+ * Add codecov workflow [Pavel Liashkov]
96
+ * Add codecov badge [Pavel Liashkov]
97
+ * Add codecov settings [Pavel Liashkov]
98
+ * Bump version [Pavel Liashkov]
99
+
100
+
101
+ ## 0.5.4 (2021-02-15)
102
+
103
+ * Bump transitions from 0.8.6 to 0.8.7 [dependabot[bot]]
104
+ * Bump version [Pavel Liashkov]
105
+
106
+
107
+ ## 0.5.2 (2021-02-02)
108
+
109
+ * Bump sqlalchemy from 1.3.22 to 1.3.23 [dependabot[bot]]
110
+ * Bump version [Pavel Liashkov]
111
+
112
+
113
+ ## 0.5.1 (2021-02-01)
114
+
115
+ * Add download count in readme [Pavel Liashkov]
116
+ * Bump version [Pavel Liashkov]
117
+
118
+
119
+ ## 0.5.0 (2021-02-01)
120
+
121
+ * Fix package format [Pavel Liashkov]
122
+ * Add licence link to readme [Pavel Liashkov]
123
+ * Change readme format [Pavel Liashkov]
124
+ * Add link on security policy [Pavel Liashkov]
125
+ * NOTASK: Bump version [Pavel Liashkov]
126
+
127
+
128
+ ## 0.4.0 (2021-01-13)
129
+
130
+ * Add state mixin [Pavel Liashkov]
131
+
132
+
133
+ ## 0.3.3 (2021-01-13)
134
+
135
+ * Update readme [Pavel Liashkov]
136
+
137
+
138
+ ## 0.3.2 (2021-01-13)
139
+
140
+ * Fix rst error in readme [Pavel Liashkov]
141
+
142
+
143
+ ## 0.3.1 (2021-01-13)
144
+
145
+ * Fix icon url in readme [Pavel Liashkov]
146
+
147
+
148
+ ## 0.3.0 (2021-01-13)
149
+
150
+ * Fix github actions [Pavel Liashkov]
151
+ * Add separeted steps lint and tests [Pavel Liashkov]
152
+ * Rename git action job [Pavel Liashkov]
153
+ * Update readme [Pavel Liashkov]
154
+
155
+
156
+ ## 0.2.1 (2021-01-08)
157
+
158
+ * Added dependency between jobs in workflow [Pavel Liashkov]
159
+ * Bump version [Pavel Liashkov]
160
+
161
+
162
+ ## 0.2.0 (2021-01-08)
163
+
164
+ * Fix dependabot config [Pavel Liashkov]
165
+ * Fix python version in pyproject [Pavel Liashkov]
166
+ * Fix types [Pavel Liashkov]
167
+ * Add LICENSE, fix setup for pypi [Pavel Liashkov]
168
+ * Bump version [Pavel Liashkov]
169
+
170
+
171
+ ## 0.1.6 (2021-01-06)
172
+
173
+ * Add config for dependabot [Pavel Liashkov]
174
+
175
+
176
+ ## 0.1.5 (2021-01-06)
177
+
178
+ * Bump version [Pavel Liashkov]
179
+
180
+
181
+ ## 0.1.3 (2021-01-05)
182
+
183
+ * Fix requires [Pavel Liashkov]
184
+
185
+
186
+ ## 0.1.2 (2021-01-05)
187
+
188
+ * Fix version settings [Pavel Liashkov]
189
+
@@ -0,0 +1,134 @@
1
+ # =============================================================================
2
+ # Project configuration
3
+ # =============================================================================
4
+ PROJECT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
5
+ PROJECT_NAME := sqlalchemy-state-machine
6
+ SRC_PATH := src/sqlalchemy_state_machine
7
+ LINT_PATHS := src tests examples
8
+ UV ?= uv
9
+
10
+ .DEFAULT_GOAL := help
11
+
12
+ .PHONY: help venv/install/main venv/install/all lint/ruff lint/mypy lint format clean test build sys/changelog sys/tag
13
+
14
+ # =============================================================================
15
+ # Help
16
+ # =============================================================================
17
+ help: ## Display this help message
18
+ @awk 'BEGIN { FS = ":.*##"; printf "\nUsage:\n make <target>\n\nTargets:\n" } /^[a-zA-Z0-9_./-]+:.*##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
19
+
20
+ # =============================================================================
21
+ # Environment
22
+ # =============================================================================
23
+ venv/install/main: ## Install main dependencies
24
+ @echo "==> Installing $(PROJECT_NAME) main dependencies"
25
+ $(UV) sync --no-group dev
26
+
27
+ venv/install/all: ## Install all dependency groups
28
+ @echo "==> Installing $(PROJECT_NAME) with all dependency groups"
29
+ $(UV) sync --all-groups
30
+
31
+ # =============================================================================
32
+ # Code quality
33
+ # =============================================================================
34
+ lint/ruff: ## Check formatting and lint with Ruff
35
+ @echo "==> Checking formatting and linting with Ruff"
36
+ $(UV) run --locked ruff format --check $(LINT_PATHS)
37
+ $(UV) run --locked ruff check $(LINT_PATHS)
38
+
39
+ lint/mypy: ## Type-check the source package with mypy
40
+ @echo "==> Type-checking $(SRC_PATH) with mypy"
41
+ $(UV) run --locked mypy $(SRC_PATH)
42
+
43
+ lint: lint/ruff lint/mypy ## Run all lint checks
44
+ @echo "==> Lint checks passed"
45
+
46
+ format: ## Format and auto-fix lint issues
47
+ @echo "==> Formatting and auto-fixing lint issues"
48
+ $(UV) run --locked ruff format $(LINT_PATHS)
49
+ $(UV) run --locked ruff check --fix $(LINT_PATHS)
50
+
51
+ # =============================================================================
52
+ # Test and build
53
+ # =============================================================================
54
+ test: ## Run the test suite with coverage
55
+ @echo "==> Testing $(PROJECT_NAME) with coverage"
56
+ $(UV) run --locked pytest --cov=sqlalchemy_state_machine --cov-report=term-missing
57
+
58
+ build: ## Build distribution packages
59
+ @echo "==> Building $(PROJECT_NAME) distribution packages"
60
+ $(UV) build
61
+
62
+ clean: ## Remove generated files and caches
63
+ @echo "==> Removing generated files and caches"
64
+ rm -rf .coverage .mypy_cache .pytest_cache .ruff_cache build dist
65
+ find . -type d -name __pycache__ -prune -exec rm -rf {} +
66
+
67
+ # =============================================================================
68
+ # Release automation
69
+ # =============================================================================
70
+ # Requires mktemp, available on the project's supported macOS and Linux development environments.
71
+ sys/changelog: ## Generate CHANGELOG.md from historical version tags
72
+ @echo "==> Generating CHANGELOG.md from version tags"
73
+ @tmp_file=$$(mktemp CHANGELOG.md.XXXXXX) || exit $$?; \
74
+ cleanup() { rm -f "$$tmp_file"; }; \
75
+ trap cleanup 0; \
76
+ trap 'exit 1' HUP INT TERM; \
77
+ tags="$$( $(UV) run --locked python -c 'import re, subprocess, sys; pattern = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$$"); raw = subprocess.check_output(["git", "tag"], text=True).splitlines(); tagged = [(tuple(map(int, match.groups())), tag) for tag in raw if (match := pattern.fullmatch(tag))]; len({version for version, _ in tagged}) == len(tagged) or sys.exit("duplicate semantic-version tags are not supported"); tagged.sort(reverse=True); print(" ".join(tag for _, tag in tagged))' )" || exit $$?; \
78
+ if [ -z "$$tags" ]; then \
79
+ echo "No semantic-version tags found." >&2; \
80
+ exit 1; \
81
+ fi; \
82
+ { \
83
+ printf '# Changelog\n\n'; \
84
+ set -- $$tags; \
85
+ newest_tag=$$1; \
86
+ unreleased=$$(git log --no-merges "$$newest_tag..HEAD" --format='* %s [%an]' --reverse) || exit $$?; \
87
+ if [ -n "$$unreleased" ]; then \
88
+ printf '## Unreleased\n\n%s\n\n' "$$unreleased"; \
89
+ fi; \
90
+ while [ $$# -gt 0 ]; do \
91
+ current_tag=$$1; \
92
+ shift; \
93
+ older_tag=$${1:-}; \
94
+ tag_date=$$(git log -1 --format=%as "$$current_tag") || exit $$?; \
95
+ display_tag=$${current_tag#v}; \
96
+ printf '## %s (%s)\n\n' "$$display_tag" "$$tag_date"; \
97
+ if [ -n "$$older_tag" ]; then range="$$older_tag..$$current_tag"; else range="$$current_tag"; fi; \
98
+ git log --no-merges "$$range" --format='* %s [%an]' --reverse || exit $$?; \
99
+ printf '\n'; \
100
+ done; \
101
+ } > "$$tmp_file" || exit $$?; \
102
+ if ! mv "$$tmp_file" CHANGELOG.md; then \
103
+ echo "Unable to replace CHANGELOG.md." >&2; \
104
+ exit 1; \
105
+ fi; \
106
+ trap - 0; \
107
+ echo "CHANGELOG.md generated successfully."
108
+
109
+ sys/tag: ## Create and push a vX.Y.Z release tag
110
+ @read -r -p "Enter tag version (e.g., 2.0.0): " TAG; \
111
+ if ! printf '%s\n' "$$TAG" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$$'; then \
112
+ echo "Tag version must use the X.Y.Z format (for example, 2.0.0)" >&2; \
113
+ exit 1; \
114
+ fi; \
115
+ project_version="$$( $(UV) run --locked python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])' )" || exit $$?; \
116
+ if [ "$$project_version" != "$$TAG" ]; then \
117
+ echo "Tag version $$TAG does not match pyproject.toml project.version $$project_version" >&2; \
118
+ exit 1; \
119
+ fi; \
120
+ if [ -n "$$(git status --porcelain --untracked-files=all)" ]; then \
121
+ echo "Worktree must be clean before creating a release tag." >&2; \
122
+ exit 1; \
123
+ fi; \
124
+ if git show-ref --verify --quiet "refs/tags/v$$TAG"; then \
125
+ echo "Local tag v$$TAG already exists." >&2; \
126
+ exit 1; \
127
+ fi; \
128
+ echo "==> Creating and pushing v$$TAG"; \
129
+ git tag -a "v$$TAG" -m "Release v$$TAG" || exit $$?; \
130
+ if ! git push origin "v$$TAG"; then \
131
+ echo "Push failed; deleting local tag v$$TAG." >&2; \
132
+ git tag -d "v$$TAG" || exit $$?; \
133
+ exit 1; \
134
+ fi
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-state-machine
3
+ Version: 2.0.0
4
+ Summary: Explicit, typed state transitions for SQLAlchemy models
5
+ Project-URL: Changelog, https://github.com/bigbag/sqlalchemy-state-machine/blob/main/CHANGELOG.md
6
+ Project-URL: Homepage, https://github.com/bigbag/sqlalchemy-state-machine
7
+ Project-URL: Issues, https://github.com/bigbag/sqlalchemy-state-machine/issues
8
+ Author-email: Pavel Liashkov <pavel.liashkov@protonmail.com>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: sqlalchemy,state-machine,transitions,workflow
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: sqlalchemy>=2.0.51
24
+ Description-Content-Type: text/markdown
25
+
26
+ # sqlalchemy-state-machine
27
+
28
+ Explicit, typed state transitions for SQLAlchemy models.
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.11+ (CI validates Python 3.11, 3.12, 3.13, and 3.14)
33
+ - SQLAlchemy 2.x
34
+
35
+ ## Install
36
+
37
+ ```console
38
+ uv add sqlalchemy-state-machine
39
+ ```
40
+
41
+ ```console
42
+ pip install sqlalchemy-state-machine
43
+ ```
44
+
45
+ ## Quick start
46
+
47
+ Declare a SQLAlchemy 2.x model and its state machine together:
48
+
49
+ ```python
50
+ from typing import ClassVar
51
+
52
+ from sqlalchemy import create_engine
53
+ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
54
+
55
+ from sqlalchemy_state_machine import StateMachine, StateMachineMixin, Transition
56
+
57
+
58
+ class Base(DeclarativeBase):
59
+ pass
60
+
61
+
62
+ class Order(StateMachineMixin, Base):
63
+ __tablename__ = "orders"
64
+
65
+ id: Mapped[int] = mapped_column(primary_key=True)
66
+ status: Mapped[str] = mapped_column(nullable=False)
67
+
68
+ state_machine: ClassVar[StateMachine] = StateMachine(
69
+ state_attribute="status",
70
+ initial="draft",
71
+ transitions=(
72
+ Transition(name="submit", source="draft", target="submitted"),
73
+ Transition(
74
+ name="cancel",
75
+ source=("draft", "submitted"),
76
+ target="cancelled",
77
+ ),
78
+ ),
79
+ )
80
+
81
+
82
+ engine = create_engine("sqlite://")
83
+ Base.metadata.create_all(engine)
84
+
85
+ with Session(engine) as session:
86
+ order = Order()
87
+ assert order.status == "draft"
88
+ assert order.can_transition("submit")
89
+ assert order.available_transitions() == ("submit", "cancel")
90
+ assert order.transition("submit") == "submitted"
91
+
92
+ session.add(order)
93
+ session.commit()
94
+ ```
95
+
96
+ `transition(name)` applies a declared transition and returns its target state.
97
+ `can_transition(name)` reports whether a transition is currently available, and
98
+ `available_transitions()` returns available names in declaration order. Calling
99
+ an unknown or unavailable transition raises `InvalidTransitionError`:
100
+
101
+ ```python
102
+ from sqlalchemy_state_machine import InvalidTransitionError
103
+
104
+ try:
105
+ order.transition("submit")
106
+ except InvalidTransitionError:
107
+ pass
108
+ ```
109
+
110
+ ## Callbacks and transactions
111
+
112
+ A `Transition` can define `before` and `after` callbacks. Both receive
113
+ `(model, transition)`. The `before` callback runs while the model has its
114
+ source state, the state is then changed, and the `after` callback runs with the
115
+ target state. If either callback raises, the model state is restored to its
116
+ original value and the exception is re-raised.
117
+
118
+ The library changes only the mapped state attribute. It owns no SQLAlchemy
119
+ session `flush`, `commit`, or `rollback`; application code controls transaction
120
+ boundaries.
121
+
122
+ ## Migrating from 1.x
123
+
124
+ 2.0 is a breaking rewrite for Python 3.11+ and SQLAlchemy 2.x.
125
+
126
+ | 1.x | 2.0 |
127
+ | --- | --- |
128
+ | `StateConfig` | `StateMachine` and `Transition` |
129
+ | `StateMixin` | `StateMachineMixin` |
130
+ | Manual SQLAlchemy init/load events | No longer needed |
131
+ | Dynamic `model.set_sent()` | `model.transition("set_sent")` |
132
+ | Redundant declared states | Inferred from the initial state and transitions |
133
+ | `after_state_change` | Per-transition `before` and `after` callbacks |
134
+
135
+ The legacy API and its `transitions` dependency were removed; update models to
136
+ the explicit 2.0 API rather than mixing 1.x declarations with 2.0 classes.
137
+
138
+ ## Example
139
+
140
+ Run the standalone example:
141
+
142
+ ```console
143
+ uv run python examples/basic_usage.py
144
+ ```
145
+
146
+ ## Development
147
+
148
+ ```console
149
+ uv sync
150
+ uv run ruff format --check src tests examples
151
+ uv run ruff check src tests examples
152
+ uv run mypy src
153
+ uv run pytest --cov=sqlalchemy_state_machine
154
+ uv build
155
+ ```
156
+
157
+ ## License
158
+
159
+ sqlalchemy-state-machine is distributed under the Apache License 2.0.