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.
Files changed (91) hide show
  1. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/dependabot.yml +0 -1
  2. pico_sqlalchemy-0.5.0/.github/workflows/ci.yml +90 -0
  3. pico_sqlalchemy-0.5.0/.github/workflows/codeql.yml +21 -0
  4. pico_sqlalchemy-0.5.0/.github/workflows/docs.yml +48 -0
  5. pico_sqlalchemy-0.5.0/.github/workflows/publish-to-pypi.yml +24 -0
  6. pico_sqlalchemy-0.5.0/.sonarcloud.properties +3 -0
  7. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/AGENTS.md +4 -3
  8. {pico_sqlalchemy-0.3.0/docs → pico_sqlalchemy-0.5.0}/CHANGELOG.md +19 -0
  9. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/CLAUDE.md +2 -2
  10. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/PKG-INFO +42 -20
  11. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/README.md +37 -18
  12. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0/docs}/CHANGELOG.md +19 -0
  13. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/architecture.md +26 -2
  14. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/hooks.py +0 -1
  15. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/how-to/alembic.md +13 -0
  16. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/index.md +2 -0
  17. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/overview.md +1 -1
  18. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/configuration.md +2 -0
  19. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/repository.md +1 -1
  20. pico_sqlalchemy-0.5.0/docs/requirements.txt +7 -0
  21. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/skills.md +2 -2
  22. pico_sqlalchemy-0.5.0/docs/troubleshooting.md +50 -0
  23. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/main.py +3 -9
  24. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/repositories.py +3 -1
  25. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/mkdocs.yml +5 -0
  26. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/pyproject.toml +6 -1
  27. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/__init__.py +4 -1
  28. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/_version.py +1 -0
  29. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/config.py +7 -0
  30. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/factory.py +12 -2
  31. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/interceptor.py +36 -6
  32. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/migrations.py +46 -0
  33. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/repository_interceptor.py +6 -19
  34. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/PKG-INFO +42 -20
  35. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/SOURCES.txt +8 -2
  36. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/requires.txt +5 -1
  37. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy.egg-info/scm_file_list.json +80 -0
  38. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy.egg-info/scm_version.json +8 -0
  39. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/conftest.py +11 -0
  40. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_coverage_boost_v2.py +4 -4
  41. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_interceptor.py +5 -5
  42. pico_sqlalchemy-0.5.0/tests/test_migrations.py +140 -0
  43. pico_sqlalchemy-0.5.0/tests/test_transaction_scope.py +133 -0
  44. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tox.ini +2 -0
  45. pico_sqlalchemy-0.3.0/.github/workflows/ci.yml +0 -125
  46. pico_sqlalchemy-0.3.0/.github/workflows/codeql.yml +0 -28
  47. pico_sqlalchemy-0.3.0/.github/workflows/docs.yml +0 -104
  48. pico_sqlalchemy-0.3.0/.github/workflows/publish-to-pypi.yml +0 -36
  49. pico_sqlalchemy-0.3.0/.github/workflows/sync-keywords.yml +0 -80
  50. pico_sqlalchemy-0.3.0/docs/requirements.txt +0 -7
  51. pico_sqlalchemy-0.3.0/src/pico_sqlalchemy/_version.py +0 -1
  52. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.coveragerc +0 -0
  53. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  54. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  55. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  56. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  57. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/CONTRIBUTING.md +0 -0
  58. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/LICENSE +0 -0
  59. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/MANIFEST.in +0 -0
  60. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/SECURITY.md +0 -0
  61. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/development/project-tooling.md +0 -0
  62. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/faq.md +0 -0
  63. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/how-to/multiple-databases.md +0 -0
  64. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/how-to/testing.md +0 -0
  65. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/javascripts/extra.js +0 -0
  66. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/migration.md +0 -0
  67. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/quickstart.md +0 -0
  68. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/declarative-base.md +0 -0
  69. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/reference/transactions.md +0 -0
  70. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/docs/stylesheets/extra.css +0 -0
  71. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/README.md +0 -0
  72. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/__init__.py +0 -0
  73. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/models.py +0 -0
  74. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/services.py +0 -0
  75. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/config.yml +0 -0
  76. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/requirements.txt +0 -0
  77. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/setup.cfg +0 -0
  78. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/base.py +0 -0
  79. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/decorators.py +0 -0
  80. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/paging.py +0 -0
  81. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/py.typed +0 -0
  82. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/session.py +0 -0
  83. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/dependency_links.txt +0 -0
  84. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/entry_points.txt +0 -0
  85. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/top_level.txt +0 -0
  86. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_ioc_integration.py +0 -0
  87. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_pagination_sort.py +0 -0
  88. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_interceptor_coverage.py +0 -0
  89. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_query.py +0 -0
  90. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_session_propagation.py +0 -0
  91. {pico_sqlalchemy-0.3.0 → pico_sqlalchemy-0.5.0}/tests/test_transaction_manager.py +0 -0
@@ -4,7 +4,6 @@ updates:
4
4
  directory: "/"
5
5
  schedule:
6
6
  interval: "weekly"
7
-
8
7
  - package-ecosystem: "pip"
9
8
  directory: "/"
10
9
  schedule:
@@ -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
@@ -0,0 +1,3 @@
1
+ sonar.python.version=3.11,3.12,3.13,3.14
2
+ sonar.sources=src
3
+ sonar.tests=tests
@@ -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` for session propagation. Separate from pico-ioc's "transaction" scope (which is for DI caching)
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` ContextVar is intentional and separate from pico-ioc scope system
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.0`
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 for session propagation, NOT the same as pico-ioc's "transaction" scope
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.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.0
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
- # 📦 pico-sqlalchemy
63
+ # pico-sqlalchemy
61
64
 
62
65
  [![PyPI](https://img.shields.io/pypi/v/pico-sqlalchemy.svg)](https://pypi.org/project/pico-sqlalchemy/)
63
66
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/dperezcabrera/pico-sqlalchemy)
@@ -68,6 +71,7 @@ Dynamic: license-file
68
71
  [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-sqlalchemy\&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
69
72
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-sqlalchemy\&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
70
73
  [![Docs](https://img.shields.io/badge/Docs-pico--sqlalchemy-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-sqlalchemy/)
74
+ [![Interactive Lab](https://img.shields.io/badge/Learn-online-green?style=flat&logo=python&logoColor=white)](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
- > 🐍 **Requires Python 3.11+**
79
- > 🚀 **Async-Native:** Built entirely on `AsyncSession` and `create_async_engine`.
80
- > **Zero-Boilerplate:** Repositories are transactional by default.
81
- > 🔍 **Declarative Queries:** Define SQL or expressions in decorators; the library executes them for you.
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
- ## 🎯 Why pico-sqlalchemy?
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
- ## 🧱 Core Features
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
- ## 📦 Installation
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
- ## 🚀 Quick Example
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
- ## Transaction Hierarchy & Rules
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
- 👉 **Result:** Active Read-Write Transaction (Implicit from `@repository`).
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
- 👉 **Result:** Active Read-Only Transaction (Implicit from `@query`).
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
- 👉 **Result:** Active Read-Only Transaction (Explicit override).
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
- ## 🔍 Declarative Queries in Depth
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
- ## 🧪 Testing
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
- ## 💡 Architecture Overview
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-framework components |
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
- ## 📝 License
382
+ ## License
361
383
 
362
384
  MIT
@@ -1,4 +1,4 @@
1
- # 📦 pico-sqlalchemy
1
+ # pico-sqlalchemy
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/pico-sqlalchemy.svg)](https://pypi.org/project/pico-sqlalchemy/)
4
4
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/dperezcabrera/pico-sqlalchemy)
@@ -9,6 +9,7 @@
9
9
  [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-sqlalchemy\&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
10
10
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-sqlalchemy\&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-sqlalchemy)
11
11
  [![Docs](https://img.shields.io/badge/Docs-pico--sqlalchemy-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-sqlalchemy/)
12
+ [![Interactive Lab](https://img.shields.io/badge/Learn-online-green?style=flat&logo=python&logoColor=white)](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
- > 🐍 **Requires Python 3.11+**
20
- > 🚀 **Async-Native:** Built entirely on `AsyncSession` and `create_async_engine`.
21
- > **Zero-Boilerplate:** Repositories are transactional by default.
22
- > 🔍 **Declarative Queries:** Define SQL or expressions in decorators; the library executes them for you.
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
- ## 🎯 Why pico-sqlalchemy?
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
- ## 🧱 Core Features
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
- ## 📦 Installation
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
- ## 🚀 Quick Example
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
- ## Transaction Hierarchy & Rules
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
- 👉 **Result:** Active Read-Write Transaction (Implicit from `@repository`).
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
- 👉 **Result:** Active Read-Only Transaction (Implicit from `@query`).
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
- 👉 **Result:** Active Read-Only Transaction (Explicit override).
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
- ## 🔍 Declarative Queries in Depth
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
- ## 🧪 Testing
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
- ## 💡 Architecture Overview
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-framework components |
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
- ## 📝 License
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 **separate from pico-ioc's scope system**. It is a lightweight, per-async-task variable that stores the currently active `AsyncSession` wrapped in a `TransactionContext`.
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 not pico-ioc scopes?** The `_tx_context` ContextVar provides transaction propagation semantics (REQUIRED, REQUIRES_NEW, etc.) that don't map to pico-ioc's scope lifecycle. A transaction may be suspended and restored (REQUIRES_NEW, NOT_SUPPORTED), which requires explicit save/restore of the context — something ContextVar handles naturally.
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
 
@@ -5,4 +5,3 @@ import logging
5
5
  # griffe's logger is patched by mkdocstrings to use mkdocs.plugins.* namespace.
6
6
  for name in ("griffe", "mkdocs.plugins.griffe", "_griffe"):
7
7
  logging.getLogger(name).setLevel(logging.ERROR)
8
-