pico-sqlalchemy 0.3.0__tar.gz → 0.5.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.
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/dependabot.yml +0 -1
- pico_sqlalchemy-0.5.0/.github/workflows/ci.yml +90 -0
- pico_sqlalchemy-0.5.0/.github/workflows/codeql.yml +21 -0
- pico_sqlalchemy-0.5.0/.github/workflows/docs.yml +48 -0
- pico_sqlalchemy-0.5.0/.github/workflows/publish-to-pypi.yml +24 -0
- pico_sqlalchemy-0.5.0/.sonarcloud.properties +3 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/AGENTS.md +4 -3
- {pico_sqlalchemy-0.3.0/docs → pico_sqlalchemy-0.5.0}/CHANGELOG.md +19 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/CLAUDE.md +2 -2
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/PKG-INFO +42 -20
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/README.md +37 -18
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0/docs}/CHANGELOG.md +19 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/architecture.md +26 -2
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/hooks.py +0 -1
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/how-to/alembic.md +13 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/index.md +2 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/overview.md +1 -1
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/configuration.md +2 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/repository.md +1 -1
- pico_sqlalchemy-0.5.0/docs/requirements.txt +7 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/skills.md +2 -2
- pico_sqlalchemy-0.5.0/docs/troubleshooting.md +50 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/main.py +3 -9
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/repositories.py +3 -1
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/mkdocs.yml +5 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/pyproject.toml +6 -1
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/__init__.py +4 -1
- pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/_version.py +1 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/config.py +7 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/factory.py +12 -2
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/interceptor.py +36 -6
- pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/migrations.py +46 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/repository_interceptor.py +6 -19
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/PKG-INFO +42 -20
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/SOURCES.txt +8 -2
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/requires.txt +5 -1
- pico_sqlalchemy-0.5.0/src/pico_sqlalchemy.egg-info/scm_file_list.json +80 -0
- pico_sqlalchemy-0.5.0/src/pico_sqlalchemy.egg-info/scm_version.json +8 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/conftest.py +11 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_coverage_boost_v2.py +4 -4
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_interceptor.py +5 -5
- pico_sqlalchemy-0.5.0/tests/test_migrations.py +140 -0
- pico_sqlalchemy-0.5.0/tests/test_transaction_scope.py +133 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tox.ini +2 -0
- pico_sqlalchemy-0.3.0/.github/workflows/ci.yml +0 -125
- pico_sqlalchemy-0.3.0/.github/workflows/codeql.yml +0 -28
- pico_sqlalchemy-0.3.0/.github/workflows/docs.yml +0 -104
- pico_sqlalchemy-0.3.0/.github/workflows/publish-to-pypi.yml +0 -36
- pico_sqlalchemy-0.3.0/.github/workflows/sync-keywords.yml +0 -80
- pico_sqlalchemy-0.3.0/docs/requirements.txt +0 -7
- pico_sqlalchemy-0.3.0/src/pico_sqlalchemy/_version.py +0 -1
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.coveragerc +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/CONTRIBUTING.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/LICENSE +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/MANIFEST.in +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/SECURITY.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/development/project-tooling.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/faq.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/how-to/multiple-databases.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/how-to/testing.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/javascripts/extra.js +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/migration.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/quickstart.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/declarative-base.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/transactions.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/stylesheets/extra.css +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/README.md +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/__init__.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/models.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/services.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/config.yml +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/requirements.txt +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/setup.cfg +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/base.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/decorators.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/paging.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/py.typed +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/session.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/dependency_links.txt +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/entry_points.txt +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/top_level.txt +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_ioc_integration.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_pagination_sort.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_interceptor_coverage.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_query.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_session_propagation.py +0 -0
- {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_transaction_manager.py +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
- uses: actions/setup-python@v6
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.11"
|
|
16
|
+
- run: pip install ruff
|
|
17
|
+
- run: ruff check src/ tests/
|
|
18
|
+
- run: ruff format --check src/ tests/
|
|
19
|
+
|
|
20
|
+
tests:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
with:
|
|
28
|
+
fetch-depth: 0
|
|
29
|
+
- uses: actions/setup-python@v6
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
- run: pip install tox
|
|
33
|
+
- name: Run tests with coverage
|
|
34
|
+
env:
|
|
35
|
+
SETUPTOOLS_SCM_PRETEND_VERSION: "0.1.0"
|
|
36
|
+
run: |
|
|
37
|
+
tox -e cov
|
|
38
|
+
mv .coverage .coverage.${{ matrix.python-version }}
|
|
39
|
+
- uses: actions/upload-artifact@v7
|
|
40
|
+
with:
|
|
41
|
+
name: coverage-${{ matrix.python-version }}
|
|
42
|
+
path: .coverage.${{ matrix.python-version }}
|
|
43
|
+
include-hidden-files: true
|
|
44
|
+
if-no-files-found: error
|
|
45
|
+
|
|
46
|
+
coverage:
|
|
47
|
+
needs: tests
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v7
|
|
51
|
+
- uses: actions/setup-python@v6
|
|
52
|
+
with:
|
|
53
|
+
python-version: "3.12"
|
|
54
|
+
- uses: actions/download-artifact@v8
|
|
55
|
+
with:
|
|
56
|
+
path: coverage-reports
|
|
57
|
+
- name: Combine and upload
|
|
58
|
+
run: |
|
|
59
|
+
pip install coverage
|
|
60
|
+
files=$(find coverage-reports -type f -name ".coverage.*")
|
|
61
|
+
if [ -z "$files" ]; then echo "No coverage files"; exit 1; fi
|
|
62
|
+
coverage combine $files
|
|
63
|
+
coverage xml
|
|
64
|
+
- uses: codecov/codecov-action@v7
|
|
65
|
+
continue-on-error: true
|
|
66
|
+
with:
|
|
67
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
68
|
+
files: coverage.xml
|
|
69
|
+
fail_ci_if_error: false
|
|
70
|
+
|
|
71
|
+
sonar:
|
|
72
|
+
if: github.event_name == 'push'
|
|
73
|
+
needs: tests
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v7
|
|
77
|
+
with:
|
|
78
|
+
fetch-depth: 0
|
|
79
|
+
- uses: SonarSource/sonarqube-scan-action@v6
|
|
80
|
+
env:
|
|
81
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
82
|
+
with:
|
|
83
|
+
args: >
|
|
84
|
+
-Dsonar.host.url=https://sonarcloud.io
|
|
85
|
+
-Dsonar.organization=dperezcabrera
|
|
86
|
+
-Dsonar.projectKey=dperezcabrera_${{ github.event.repository.name }}
|
|
87
|
+
-Dsonar.projectName=${{ github.event.repository.name }}
|
|
88
|
+
-Dsonar.sources=src
|
|
89
|
+
-Dsonar.tests=tests
|
|
90
|
+
-Dsonar.python.version=3.11,3.12,3.13,3.14
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [main]
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 6 * * 1"
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
security-events: write
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
analyze:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- uses: github/codeql-action/init@v4
|
|
20
|
+
with: { languages: python }
|
|
21
|
+
- uses: github/codeql-action/analyze@v4
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
paths: [docs/**, mkdocs.yml]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
paths: [docs/**, mkdocs.yml]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
pages: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: pages
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
validate:
|
|
22
|
+
if: github.event_name == 'pull_request'
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
with: { fetch-depth: 0 }
|
|
27
|
+
- uses: actions/setup-python@v6
|
|
28
|
+
with: { python-version: "3.12" }
|
|
29
|
+
- run: pip install -r docs/requirements.txt
|
|
30
|
+
- run: mkdocs build --strict
|
|
31
|
+
|
|
32
|
+
deploy:
|
|
33
|
+
if: github.event_name != 'pull_request'
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment:
|
|
36
|
+
name: github-pages
|
|
37
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
with: { fetch-depth: 0 }
|
|
41
|
+
- uses: actions/setup-python@v6
|
|
42
|
+
with: { python-version: "3.12" }
|
|
43
|
+
- run: pip install -r docs/requirements.txt
|
|
44
|
+
- run: mkdocs build --strict --verbose
|
|
45
|
+
- uses: actions/upload-pages-artifact@v5
|
|
46
|
+
with: { path: site }
|
|
47
|
+
- id: deployment
|
|
48
|
+
uses: actions/deploy-pages@v5
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
id-token: write
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v7
|
|
15
|
+
with: { fetch-depth: 0 }
|
|
16
|
+
- uses: actions/setup-python@v6
|
|
17
|
+
with: { python-version: "3.x" }
|
|
18
|
+
- run: pip install build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
21
|
+
with:
|
|
22
|
+
skip-existing: true
|
|
23
|
+
verify-metadata: true
|
|
24
|
+
attestations: true
|
|
@@ -35,9 +35,10 @@ src/pico_sqlalchemy/
|
|
|
35
35
|
- **Propagation modes**: REQUIRED, REQUIRES_NEW, MANDATORY, NEVER, NOT_SUPPORTED, SUPPORTS
|
|
36
36
|
- **Priority chain**: `@transactional` > `@query` (read-only) > `@repository` (read-write)
|
|
37
37
|
- **`SessionManager`**: Created by `SqlAlchemyFactory` (not `@component`). Manages engine, sessions, transactions
|
|
38
|
-
- **`_tx_context` ContextVar**: Holds `TransactionContext`
|
|
38
|
+
- **`_tx_context` ContextVar**: Holds `TransactionContext` (the session) for propagation across nested calls — the *session* side of a transaction
|
|
39
|
+
- **`"transaction"` DI scope**: `TransactionalInterceptor` activates pico-ioc's `"transaction"` scope (with `cleanup=True`) whenever a *new* transaction starts (REQUIRES_NEW, or REQUIRED with no enclosing tx). So a `scope="transaction"` component is one instance per transaction (Unit-of-Work / identity-map), released with its `@cleanup` hooks when the tx ends. The session ContextVar and the DI scope are two facets of one boundary
|
|
39
40
|
- **`get_session(manager)`**: Returns current session from active transaction context
|
|
40
|
-
- **Non-transactional paths** (NEVER, NOT_SUPPORTED, SUPPORTS without tx): Still set `TransactionContext` so `get_session()` works
|
|
41
|
+
- **Non-transactional paths** (NEVER, NOT_SUPPORTED, SUPPORTS without tx): Still set `TransactionContext` so `get_session()` works, but open no DI `"transaction"` scope (resolving a `scope="transaction"` component there raises `ScopeError`)
|
|
41
42
|
|
|
42
43
|
## Code Style
|
|
43
44
|
|
|
@@ -59,4 +60,4 @@ src/pico_sqlalchemy/
|
|
|
59
60
|
|
|
60
61
|
- Do not modify `_version.py`
|
|
61
62
|
- Do not add `@component` to `SessionManager` (factory creates it)
|
|
62
|
-
- `_tx_context`
|
|
63
|
+
- `_tx_context` (session propagation) and the pico-ioc `"transaction"` DI scope are bound to one boundary by `TransactionalInterceptor` — keep them coordinated; do not reintroduce a second, independent transaction concept
|
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.h
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `AlembicMigrator`: runs `alembic upgrade` at container startup when `database.migrations_path` points to an Alembic script directory (`migrations_target` selects the revision, default `head`). Optional extra: `pico-sqlalchemy[migrations]`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-06-07
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Transaction-scoped DI**: `TransactionalInterceptor` now binds pico-ioc's `"transaction"` DI scope to the database transaction boundary. When a *new* transaction starts (`REQUIRES_NEW`, or `REQUIRED` with no enclosing transaction) it activates a fresh `"transaction"` scope (via `container.scope(..., cleanup=True)`) for the duration of the call, so components registered with `scope="transaction"` live exactly one transaction and run their `@cleanup` hooks when it ends. Joins reuse the enclosing scope. Requires **pico-ioc >= 2.2.6**.
|
|
22
|
+
- `tests/test_transaction_scope.py`: covers one-instance-per-transaction, sharing within a transaction, `REQUIRES_NEW` scope push/restore, fail-fast resolution outside a transaction, and `@cleanup` on transaction end.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Bumped `pico-ioc` dependency to `>= 2.2.6`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
10
29
|
## [0.3.0] - 2026-02-20
|
|
11
30
|
|
|
12
31
|
### Changed
|
|
@@ -10,10 +10,10 @@ pico-sqlalchemy provides SQLAlchemy integration for pico-ioc. It uses:
|
|
|
10
10
|
|
|
11
11
|
## Key Reminders
|
|
12
12
|
|
|
13
|
-
- pico-ioc dependency: `>= 2.2.
|
|
13
|
+
- pico-ioc dependency: `>= 2.2.6` (needs `container.scope(..., cleanup=True)`)
|
|
14
14
|
- **NEVER change `version_scheme`** in pyproject.toml. It MUST remain `"post-release"`. Changing it to `"guess-next-dev"` causes `.dev0` versions to leak to PyPI. This was already fixed once — do not revert it.
|
|
15
15
|
- requires-python >= 3.11
|
|
16
16
|
- Commit messages: one line only
|
|
17
17
|
- `@transactional` works both with and without parentheses (like `@repository`)
|
|
18
18
|
- `SessionManager` has NO `@component` decorator - it's created by the factory
|
|
19
|
-
- `_tx_context` ContextVar is
|
|
19
|
+
- `_tx_context` ContextVar is the *session* side of a transaction (propagation across nested calls). `TransactionalInterceptor` binds pico-ioc's `"transaction"` DI scope to the *same* boundary: when a new transaction starts (REQUIRES_NEW, or REQUIRED with no enclosing tx) it activates a fresh `"transaction"` scope (with `cleanup=True`) for the call, so `scope="transaction"` components live exactly one transaction. They are two facets of one boundary, not separate mechanisms.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-sqlalchemy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Pico-ioc integration for SQLAlchemy. Adds Spring-style transactional support, configuration, and helpers.
|
|
5
5
|
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -47,17 +47,20 @@ Classifier: Typing :: Typed
|
|
|
47
47
|
Requires-Python: >=3.11
|
|
48
48
|
Description-Content-Type: text/markdown
|
|
49
49
|
License-File: LICENSE
|
|
50
|
-
Requires-Dist: pico-ioc>=2.2.
|
|
50
|
+
Requires-Dist: pico-ioc>=2.2.6
|
|
51
51
|
Requires-Dist: sqlalchemy>=2.0
|
|
52
|
+
Provides-Extra: migrations
|
|
53
|
+
Requires-Dist: alembic>=1.13; extra == "migrations"
|
|
52
54
|
Provides-Extra: async
|
|
53
55
|
Requires-Dist: asyncpg>=0.29.0; extra == "async"
|
|
54
56
|
Provides-Extra: test
|
|
55
57
|
Requires-Dist: pytest>=8; extra == "test"
|
|
58
|
+
Requires-Dist: alembic>=1.13; extra == "test"
|
|
56
59
|
Requires-Dist: pytest-asyncio>=0.23.5; extra == "test"
|
|
57
60
|
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
58
61
|
Dynamic: license-file
|
|
59
62
|
|
|
60
|
-
#
|
|
63
|
+
# pico-sqlalchemy
|
|
61
64
|
|
|
62
65
|
[](https://pypi.org/project/pico-sqlalchemy/)
|
|
63
66
|
[](https://deepwiki.com/dperezcabrera/pico-sqlalchemy)
|
|
@@ -68,6 +71,7 @@ Dynamic: license-file
|
|
|
68
71
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
|
|
69
72
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
|
|
70
73
|
[](https://dperezcabrera.github.io/pico-sqlalchemy/)
|
|
74
|
+
[](https://dperezcabrera.github.io/pico-learn/)
|
|
71
75
|
|
|
72
76
|
# Pico-SQLAlchemy
|
|
73
77
|
|
|
@@ -75,14 +79,14 @@ Dynamic: license-file
|
|
|
75
79
|
|
|
76
80
|
It brings constructor-based dependency injection, **implicit transaction management**, and powerful **declarative queries** using pure Python and SQLAlchemy’s Async ORM.
|
|
77
81
|
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
>
|
|
82
|
+
> **Requires Python 3.11+**
|
|
83
|
+
> **Async-Native:** Built entirely on `AsyncSession` and `create_async_engine`.
|
|
84
|
+
> **Zero-Boilerplate:** Repositories are transactional by default.
|
|
85
|
+
> **Declarative Queries:** Define SQL or expressions in decorators; the library executes them for you.
|
|
82
86
|
|
|
83
87
|
---
|
|
84
88
|
|
|
85
|
-
##
|
|
89
|
+
## Why pico-sqlalchemy?
|
|
86
90
|
|
|
87
91
|
Most Python apps suffer from manual session handling (`async with session...`), scattered transaction logic, and verbose repository patterns.
|
|
88
92
|
|
|
@@ -98,7 +102,7 @@ Most Python apps suffer from manual session handling (`async with session...`),
|
|
|
98
102
|
|
|
99
103
|
---
|
|
100
104
|
|
|
101
|
-
##
|
|
105
|
+
## Core Features
|
|
102
106
|
|
|
103
107
|
* **Implicit Transactions:** Methods inside `@repository` are automatically **Read-Write** transactional.
|
|
104
108
|
* **Declarative Queries:** Use `@query` to run SQL or Expressions automatically (defaults to **Read-Only**).
|
|
@@ -108,7 +112,7 @@ Most Python apps suffer from manual session handling (`async with session...`),
|
|
|
108
112
|
|
|
109
113
|
---
|
|
110
114
|
|
|
111
|
-
##
|
|
115
|
+
## Installation
|
|
112
116
|
|
|
113
117
|
```bash
|
|
114
118
|
pip install pico-sqlalchemy
|
|
@@ -123,7 +127,7 @@ pip install asyncpg # for PostgreSQL
|
|
|
123
127
|
|
|
124
128
|
-----
|
|
125
129
|
|
|
126
|
-
##
|
|
130
|
+
## Quick Example
|
|
127
131
|
|
|
128
132
|
### 1\. Define Model
|
|
129
133
|
|
|
@@ -218,7 +222,7 @@ if __name__ == "__main__":
|
|
|
218
222
|
|
|
219
223
|
-----
|
|
220
224
|
|
|
221
|
-
##
|
|
225
|
+
## Transaction Hierarchy & Rules
|
|
222
226
|
|
|
223
227
|
Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction configuration. The priority order (highest wins) is:
|
|
224
228
|
|
|
@@ -236,7 +240,7 @@ Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction config
|
|
|
236
240
|
async def update_user(self): ...
|
|
237
241
|
```
|
|
238
242
|
|
|
239
|
-
|
|
243
|
+
**Result:** Active Read-Write Transaction (Implicit from `@repository`).
|
|
240
244
|
|
|
241
245
|
2. **Query Method:**
|
|
242
246
|
|
|
@@ -245,7 +249,7 @@ Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction config
|
|
|
245
249
|
async def get_data(self): ...
|
|
246
250
|
```
|
|
247
251
|
|
|
248
|
-
|
|
252
|
+
**Result:** Active Read-Only Transaction (Implicit from `@query`).
|
|
249
253
|
|
|
250
254
|
3. **Manual Override:**
|
|
251
255
|
|
|
@@ -254,11 +258,29 @@ Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction config
|
|
|
254
258
|
async def complex_report(self): ...
|
|
255
259
|
```
|
|
256
260
|
|
|
257
|
-
|
|
261
|
+
**Result:** Active Read-Only Transaction (Explicit override).
|
|
262
|
+
|
|
263
|
+
### Transaction-scoped components *(v0.4.0+)*
|
|
264
|
+
|
|
265
|
+
Beyond managing the SQLAlchemy session, the interceptor binds pico-ioc's **`"transaction"` DI scope** to the same boundary. A component registered with `scope="transaction"` is instantiated **once per database transaction** and torn down (running its `@cleanup` hooks) when that transaction ends:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
@component(scope="transaction")
|
|
269
|
+
class UnitOfWorkAudit:
|
|
270
|
+
def __init__(self):
|
|
271
|
+
self.events: list[str] = []
|
|
272
|
+
|
|
273
|
+
@cleanup
|
|
274
|
+
def flush(self):
|
|
275
|
+
# runs exactly when the enclosing transaction ends
|
|
276
|
+
...
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A **new** transaction (`REQUIRES_NEW`, or `REQUIRED` with no enclosing transaction) opens a fresh scope; **joins reuse** the enclosing one — so the session boundary and the DI lifetime are two facets of a single transaction. Requires **pico-ioc ≥ 2.2.6**.
|
|
258
280
|
|
|
259
281
|
-----
|
|
260
282
|
|
|
261
|
-
##
|
|
283
|
+
## Declarative Queries in Depth
|
|
262
284
|
|
|
263
285
|
The `@query` decorator eliminates boilerplate for common fetches.
|
|
264
286
|
|
|
@@ -293,7 +315,7 @@ async def find_active(self, page: PageRequest) -> Page[User]: ...
|
|
|
293
315
|
|
|
294
316
|
-----
|
|
295
317
|
|
|
296
|
-
##
|
|
318
|
+
## Testing
|
|
297
319
|
|
|
298
320
|
Testing is simple because you can override the configuration or the components easily using Pico-IoC.
|
|
299
321
|
|
|
@@ -311,7 +333,7 @@ async def test_service():
|
|
|
311
333
|
|
|
312
334
|
-----
|
|
313
335
|
|
|
314
|
-
##
|
|
336
|
+
## Architecture Overview
|
|
315
337
|
|
|
316
338
|
```
|
|
317
339
|
┌─────────────────────────────┐
|
|
@@ -349,7 +371,7 @@ curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/instal
|
|
|
349
371
|
|---------|-------------|
|
|
350
372
|
| `/add-repository` | Add SQLAlchemy entities and repositories with transactions |
|
|
351
373
|
| `/add-component` | Add components, factories, interceptors, settings |
|
|
352
|
-
| `/add-tests` | Generate tests for pico
|
|
374
|
+
| `/add-tests` | Generate tests for pico components |
|
|
353
375
|
|
|
354
376
|
All skills: `curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash`
|
|
355
377
|
|
|
@@ -357,6 +379,6 @@ See [pico-skills](https://github.com/dperezcabrera/pico-skills) for details.
|
|
|
357
379
|
|
|
358
380
|
---
|
|
359
381
|
|
|
360
|
-
##
|
|
382
|
+
## License
|
|
361
383
|
|
|
362
384
|
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pico-sqlalchemy
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/pico-sqlalchemy/)
|
|
4
4
|
[](https://deepwiki.com/dperezcabrera/pico-sqlalchemy)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
|
|
10
10
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
|
|
11
11
|
[](https://dperezcabrera.github.io/pico-sqlalchemy/)
|
|
12
|
+
[](https://dperezcabrera.github.io/pico-learn/)
|
|
12
13
|
|
|
13
14
|
# Pico-SQLAlchemy
|
|
14
15
|
|
|
@@ -16,14 +17,14 @@
|
|
|
16
17
|
|
|
17
18
|
It brings constructor-based dependency injection, **implicit transaction management**, and powerful **declarative queries** using pure Python and SQLAlchemy’s Async ORM.
|
|
18
19
|
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
20
|
+
> **Requires Python 3.11+**
|
|
21
|
+
> **Async-Native:** Built entirely on `AsyncSession` and `create_async_engine`.
|
|
22
|
+
> **Zero-Boilerplate:** Repositories are transactional by default.
|
|
23
|
+
> **Declarative Queries:** Define SQL or expressions in decorators; the library executes them for you.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
26
|
-
##
|
|
27
|
+
## Why pico-sqlalchemy?
|
|
27
28
|
|
|
28
29
|
Most Python apps suffer from manual session handling (`async with session...`), scattered transaction logic, and verbose repository patterns.
|
|
29
30
|
|
|
@@ -39,7 +40,7 @@ Most Python apps suffer from manual session handling (`async with session...`),
|
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
42
|
-
##
|
|
43
|
+
## Core Features
|
|
43
44
|
|
|
44
45
|
* **Implicit Transactions:** Methods inside `@repository` are automatically **Read-Write** transactional.
|
|
45
46
|
* **Declarative Queries:** Use `@query` to run SQL or Expressions automatically (defaults to **Read-Only**).
|
|
@@ -49,7 +50,7 @@ Most Python apps suffer from manual session handling (`async with session...`),
|
|
|
49
50
|
|
|
50
51
|
---
|
|
51
52
|
|
|
52
|
-
##
|
|
53
|
+
## Installation
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
56
|
pip install pico-sqlalchemy
|
|
@@ -64,7 +65,7 @@ pip install asyncpg # for PostgreSQL
|
|
|
64
65
|
|
|
65
66
|
-----
|
|
66
67
|
|
|
67
|
-
##
|
|
68
|
+
## Quick Example
|
|
68
69
|
|
|
69
70
|
### 1\. Define Model
|
|
70
71
|
|
|
@@ -159,7 +160,7 @@ if __name__ == "__main__":
|
|
|
159
160
|
|
|
160
161
|
-----
|
|
161
162
|
|
|
162
|
-
##
|
|
163
|
+
## Transaction Hierarchy & Rules
|
|
163
164
|
|
|
164
165
|
Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction configuration. The priority order (highest wins) is:
|
|
165
166
|
|
|
@@ -177,7 +178,7 @@ Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction config
|
|
|
177
178
|
async def update_user(self): ...
|
|
178
179
|
```
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
**Result:** Active Read-Write Transaction (Implicit from `@repository`).
|
|
181
182
|
|
|
182
183
|
2. **Query Method:**
|
|
183
184
|
|
|
@@ -186,7 +187,7 @@ Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction config
|
|
|
186
187
|
async def get_data(self): ...
|
|
187
188
|
```
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
**Result:** Active Read-Only Transaction (Implicit from `@query`).
|
|
190
191
|
|
|
191
192
|
3. **Manual Override:**
|
|
192
193
|
|
|
@@ -195,11 +196,29 @@ Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction config
|
|
|
195
196
|
async def complex_report(self): ...
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
|
|
199
|
+
**Result:** Active Read-Only Transaction (Explicit override).
|
|
200
|
+
|
|
201
|
+
### Transaction-scoped components *(v0.4.0+)*
|
|
202
|
+
|
|
203
|
+
Beyond managing the SQLAlchemy session, the interceptor binds pico-ioc's **`"transaction"` DI scope** to the same boundary. A component registered with `scope="transaction"` is instantiated **once per database transaction** and torn down (running its `@cleanup` hooks) when that transaction ends:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
@component(scope="transaction")
|
|
207
|
+
class UnitOfWorkAudit:
|
|
208
|
+
def __init__(self):
|
|
209
|
+
self.events: list[str] = []
|
|
210
|
+
|
|
211
|
+
@cleanup
|
|
212
|
+
def flush(self):
|
|
213
|
+
# runs exactly when the enclosing transaction ends
|
|
214
|
+
...
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
A **new** transaction (`REQUIRES_NEW`, or `REQUIRED` with no enclosing transaction) opens a fresh scope; **joins reuse** the enclosing one — so the session boundary and the DI lifetime are two facets of a single transaction. Requires **pico-ioc ≥ 2.2.6**.
|
|
199
218
|
|
|
200
219
|
-----
|
|
201
220
|
|
|
202
|
-
##
|
|
221
|
+
## Declarative Queries in Depth
|
|
203
222
|
|
|
204
223
|
The `@query` decorator eliminates boilerplate for common fetches.
|
|
205
224
|
|
|
@@ -234,7 +253,7 @@ async def find_active(self, page: PageRequest) -> Page[User]: ...
|
|
|
234
253
|
|
|
235
254
|
-----
|
|
236
255
|
|
|
237
|
-
##
|
|
256
|
+
## Testing
|
|
238
257
|
|
|
239
258
|
Testing is simple because you can override the configuration or the components easily using Pico-IoC.
|
|
240
259
|
|
|
@@ -252,7 +271,7 @@ async def test_service():
|
|
|
252
271
|
|
|
253
272
|
-----
|
|
254
273
|
|
|
255
|
-
##
|
|
274
|
+
## Architecture Overview
|
|
256
275
|
|
|
257
276
|
```
|
|
258
277
|
┌─────────────────────────────┐
|
|
@@ -290,7 +309,7 @@ curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/instal
|
|
|
290
309
|
|---------|-------------|
|
|
291
310
|
| `/add-repository` | Add SQLAlchemy entities and repositories with transactions |
|
|
292
311
|
| `/add-component` | Add components, factories, interceptors, settings |
|
|
293
|
-
| `/add-tests` | Generate tests for pico
|
|
312
|
+
| `/add-tests` | Generate tests for pico components |
|
|
294
313
|
|
|
295
314
|
All skills: `curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash`
|
|
296
315
|
|
|
@@ -298,6 +317,6 @@ See [pico-skills](https://github.com/dperezcabrera/pico-skills) for details.
|
|
|
298
317
|
|
|
299
318
|
---
|
|
300
319
|
|
|
301
|
-
##
|
|
320
|
+
## License
|
|
302
321
|
|
|
303
322
|
MIT
|
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.h
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `AlembicMigrator`: runs `alembic upgrade` at container startup when `database.migrations_path` points to an Alembic script directory (`migrations_target` selects the revision, default `head`). Optional extra: `pico-sqlalchemy[migrations]`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-06-07
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Transaction-scoped DI**: `TransactionalInterceptor` now binds pico-ioc's `"transaction"` DI scope to the database transaction boundary. When a *new* transaction starts (`REQUIRES_NEW`, or `REQUIRED` with no enclosing transaction) it activates a fresh `"transaction"` scope (via `container.scope(..., cleanup=True)`) for the duration of the call, so components registered with `scope="transaction"` live exactly one transaction and run their `@cleanup` hooks when it ends. Joins reuse the enclosing scope. Requires **pico-ioc >= 2.2.6**.
|
|
22
|
+
- `tests/test_transaction_scope.py`: covers one-instance-per-transaction, sharing within a transaction, `REQUIRES_NEW` scope push/restore, fail-fast resolution outside a transaction, and `@cleanup` on transaction end.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Bumped `pico-ioc` dependency to `>= 2.2.6`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
10
29
|
## [0.3.0] - 2026-02-20
|
|
11
30
|
|
|
12
31
|
### Changed
|
|
@@ -118,7 +118,7 @@ pico-sqlalchemy uses a `ContextVar` to propagate the active session across async
|
|
|
118
118
|
_tx_context: ContextVar[TransactionContext | None]
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
This is **
|
|
121
|
+
This is the **session** side of a transaction: a lightweight, per-async-task variable that stores the currently active `AsyncSession` wrapped in a `TransactionContext`. It is paired with the pico-ioc `"transaction"` DI scope (the **component** side — see below), bound to the same boundary by `TransactionalInterceptor`.
|
|
122
122
|
|
|
123
123
|
**How it works:**
|
|
124
124
|
|
|
@@ -140,7 +140,31 @@ Service.create_user() ← @transactional
|
|
|
140
140
|
_tx_context = None
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
**Why
|
|
143
|
+
**Why a ContextVar for the session?** Transaction propagation (REQUIRED, REQUIRES_NEW, etc.) needs to suspend and restore the active session (REQUIRES_NEW, NOT_SUPPORTED) — something a `ContextVar` handles naturally, save/restore via tokens.
|
|
144
|
+
|
|
145
|
+
### The paired `"transaction"` DI scope
|
|
146
|
+
|
|
147
|
+
`TransactionalInterceptor` also binds pico-ioc's `"transaction"` **DI scope** to the same boundary. Whenever a *new* transaction is born — `REQUIRES_NEW`, or `REQUIRED` with no enclosing transaction — it activates a fresh `"transaction"` scope (with `cleanup=True`) around the call:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
Service.create_user() ← @transactional REQUIRED (new tx)
|
|
151
|
+
│ activate "transaction" scope (id = T1) _tx_context = session_A
|
|
152
|
+
│
|
|
153
|
+
├─ container.get(AuditLog) → scope="transaction" ← created once, id T1
|
|
154
|
+
├─ container.get(AuditLog) → same instance ← joined: same T1
|
|
155
|
+
│
|
|
156
|
+
├─ Other.in_new_tx() ← @transactional REQUIRES_NEW
|
|
157
|
+
│ │ activate "transaction" scope (id = T2) _tx_context = session_B
|
|
158
|
+
│ └─ container.get(AuditLog) → NEW instance ← isolated in T2
|
|
159
|
+
│ deactivate T2 (+ @cleanup), restore T1, session_A
|
|
160
|
+
│
|
|
161
|
+
└─ commit / rollback
|
|
162
|
+
deactivate T1 (runs @cleanup on T1's components), _tx_context = None
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
So a `scope="transaction"` component is **one instance per transaction** — a Unit-of-Work / identity-map that is released (running its `@cleanup` hooks) when the transaction ends. Joins reuse the enclosing instance; `REQUIRES_NEW` gets its own and restores the outer one afterwards. Non-transactional paths (`NEVER`, `NOT_SUPPORTED`, `SUPPORTS` without a tx) open no scope — resolving a `scope="transaction"` component there raises `ScopeError`.
|
|
166
|
+
|
|
167
|
+
> The scope is bound at the interceptor (which wraps the call directly), not deep inside `SessionManager`: a `ContextVar` set inside the transaction async-generator does not reliably reach the method body across nesting. As with any AOP, **self-invocation bypasses it** — `REQUIRES_NEW` only opens a new transaction/scope when the method is called on another injected component, not via `self.method()`.
|
|
144
168
|
|
|
145
169
|
---
|
|
146
170
|
|