pico-sqlalchemy 0.4.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.4.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.4.0/docs → pico_sqlalchemy-0.5.0}/CHANGELOG.md +8 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/PKG-INFO +4 -1
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0/docs}/CHANGELOG.md +8 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/hooks.py +0 -1
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/how-to/alembic.md +13 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/index.md +2 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/overview.md +1 -1
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/configuration.md +2 -0
- {pico_sqlalchemy-0.4.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.5.0/docs/troubleshooting.md +50 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/repositories.py +1 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/mkdocs.yml +5 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/pyproject.toml +5 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/__init__.py +2 -0
- pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/_version.py +1 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/config.py +7 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/factory.py +9 -1
- pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/migrations.py +46 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/repository_interceptor.py +6 -19
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/PKG-INFO +4 -1
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/SOURCES.txt +6 -1
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/requires.txt +4 -0
- 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.5.0/tests/test_migrations.py +140 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tox.ini +2 -0
- pico_sqlalchemy-0.4.0/.github/workflows/ci.yml +0 -125
- pico_sqlalchemy-0.4.0/.github/workflows/codeql.yml +0 -28
- pico_sqlalchemy-0.4.0/.github/workflows/docs.yml +0 -104
- pico_sqlalchemy-0.4.0/.github/workflows/publish-to-pypi.yml +0 -48
- pico_sqlalchemy-0.4.0/.github/workflows/sync-keywords.yml +0 -80
- pico_sqlalchemy-0.4.0/docs/requirements.txt +0 -7
- pico_sqlalchemy-0.4.0/src/pico_sqlalchemy/_version.py +0 -1
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.coveragerc +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/AGENTS.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/CLAUDE.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/CONTRIBUTING.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/LICENSE +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/MANIFEST.in +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/README.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/SECURITY.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/architecture.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/development/project-tooling.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/faq.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/how-to/multiple-databases.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/how-to/testing.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/javascripts/extra.js +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/migration.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/quickstart.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/declarative-base.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/transactions.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/skills.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/stylesheets/extra.css +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/README.md +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/__init__.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/main.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/models.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/services.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/config.yml +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/requirements.txt +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/setup.cfg +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/base.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/decorators.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/interceptor.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/paging.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/py.typed +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/session.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/dependency_links.txt +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/entry_points.txt +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/top_level.txt +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/conftest.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_coverage_boost_v2.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_interceptor.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_ioc_integration.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_pagination_sort.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_interceptor_coverage.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_query.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_session_propagation.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_transaction_manager.py +0 -0
- {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_transaction_scope.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
|
|
@@ -7,6 +7,14 @@ 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
|
+
|
|
10
18
|
## [0.4.0] - 2026-06-07
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -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
|
|
@@ -49,10 +49,13 @@ Description-Content-Type: text/markdown
|
|
|
49
49
|
License-File: LICENSE
|
|
50
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
|
|
@@ -7,6 +7,14 @@ 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
|
+
|
|
10
18
|
## [0.4.0] - 2026-06-07
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -10,6 +10,19 @@ This guide shows how to integrate [Alembic](https://alembic.sqlalchemy.org/) dat
|
|
|
10
10
|
pip install alembic
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Zero-code startup migrations
|
|
14
|
+
|
|
15
|
+
Once your Alembic directory exists (steps below), you can skip wiring entirely: point config at it and the container upgrades on startup, before any other `DatabaseConfigurer`:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
database:
|
|
19
|
+
url: postgresql+asyncpg://user:pass@host/db
|
|
20
|
+
migrations_path: alembic # directory containing env.py
|
|
21
|
+
migrations_target: head # optional, default head
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires the extra: `pip install pico-sqlalchemy[migrations]`. The configured `database.url` is passed to Alembic verbatim as `sqlalchemy.url`, so keep the async `env.py` from this guide.
|
|
25
|
+
|
|
13
26
|
## 1. Initialize Alembic
|
|
14
27
|
|
|
15
28
|
```bash
|
|
@@ -89,3 +89,5 @@ pip install asyncpg # PostgreSQL
|
|
|
89
89
|
## License
|
|
90
90
|
|
|
91
91
|
MIT License - see LICENSE file for details.
|
|
92
|
+
|
|
93
|
+
**See it in context**: the [flagship use case](https://dperezcabrera.github.io/pico-boot/flagship/) wires this module into a full order platform together with the rest of the ecosystem.
|
|
@@ -30,6 +30,8 @@ The following fields map directly to the underlying SQLAlchemy `create_async_eng
|
|
|
30
30
|
| **`pool_size`** | `int` | `5` | The number of connections to keep open inside the connection pool. |
|
|
31
31
|
| **`pool_pre_ping`** | `bool` | `True` | If `True`, tests connections for liveness upon checkout (prevents "MySQL server has gone away" errors). |
|
|
32
32
|
| **`pool_recycle`** | `int` | `3600` | Recycle connections after this many seconds to prevent timeouts from the database side. |
|
|
33
|
+
| **`migrations_path`** | `str` | `""` | Alembic script directory (the one containing `env.py`). Empty disables startup migrations. Requires `pico-sqlalchemy[migrations]`. |
|
|
34
|
+
| **`migrations_target`** | `str` | `"head"` | Alembic revision to upgrade to on startup. |
|
|
33
35
|
|
|
34
36
|
> **Note:** `pool_size`, `pool_pre_ping`, and `pool_recycle` are ignored if using SQLite with `:memory:`.
|
|
35
37
|
|
|
@@ -142,7 +142,7 @@ class UserRepository:
|
|
|
142
142
|
|
|
143
143
|
You can request dynamic sorting by passing a list of `Sort` objects within the `PageRequest`.
|
|
144
144
|
|
|
145
|
-
>
|
|
145
|
+
> **Warning: Expression Mode Only**
|
|
146
146
|
>
|
|
147
147
|
> Dynamic sorting (injecting `ORDER BY` clauses based on `PageRequest`) works **only** in **Expression Mode (`expr`)**.
|
|
148
148
|
>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Startup migrations did not run
|
|
4
|
+
|
|
5
|
+
- `database.migrations_path` must point at the Alembic script directory —
|
|
6
|
+
the one containing `env.py`, not the project root.
|
|
7
|
+
- Alembic is an optional dependency: `pip install pico-sqlalchemy[migrations]`.
|
|
8
|
+
When the path is set without it, startup fails loudly with the install hint.
|
|
9
|
+
- `AlembicMigrator` runs at priority -100, before other `DatabaseConfigurer`
|
|
10
|
+
hooks — if a seed hook needs migrated tables, keep its priority above -100.
|
|
11
|
+
|
|
12
|
+
## Alembic fails with an async driver error
|
|
13
|
+
|
|
14
|
+
The configured `database.url` (e.g. `postgresql+asyncpg://...`) is handed to
|
|
15
|
+
Alembic verbatim as `sqlalchemy.url`. Write `env.py` for the async URL —
|
|
16
|
+
Alembic's async template, or the `env.py` from the
|
|
17
|
+
[Alembic how-to](how-to/alembic.md) — or derive a sync engine inside your
|
|
18
|
+
`env.py`.
|
|
19
|
+
|
|
20
|
+
## "attached to a different loop" with asyncpg
|
|
21
|
+
|
|
22
|
+
A `DatabaseConfigurer` that runs DDL via `asyncio.run(...)` leaves pooled
|
|
23
|
+
asyncpg connections bound to that throwaway loop; the first request then
|
|
24
|
+
fails with `got Future attached to a different loop`. End the configurer
|
|
25
|
+
with `await engine.dispose()` so request loops create fresh connections.
|
|
26
|
+
(SQLite/aiosqlite tolerates this, Postgres does not — test against the real
|
|
27
|
+
dialect.) In tests, use `with TestClient(app):` — without the context
|
|
28
|
+
manager every request gets its own event loop.
|
|
29
|
+
|
|
30
|
+
## `MissingGreenlet` or "greenlet_spawn has not been called"
|
|
31
|
+
|
|
32
|
+
An async session was used from sync code. Access the session inside
|
|
33
|
+
`@transactional` async methods, or use `run_sync` for sync-only helpers.
|
|
34
|
+
|
|
35
|
+
## My `@query` method returns nothing / the wrong type
|
|
36
|
+
|
|
37
|
+
Derived queries parse the method NAME; check the prefix (`find_by_`,
|
|
38
|
+
`count_by_`, `exists_by_`) and that parameter names match column names.
|
|
39
|
+
Explicit `@query("...")` strings bypass name parsing entirely.
|
|
40
|
+
|
|
41
|
+
## Changes are not persisted
|
|
42
|
+
|
|
43
|
+
Writes must happen inside a transaction boundary: `@transactional` on the
|
|
44
|
+
method (or an enclosing one — REQUIRED propagation reuses it). Reads outside
|
|
45
|
+
a transaction run in autocommit and see committed state only.
|
|
46
|
+
|
|
47
|
+
## `pool_size` seems ignored
|
|
48
|
+
|
|
49
|
+
With SQLite `:memory:` URLs, pool settings are not applied (single
|
|
50
|
+
connection semantics). They take effect on real server databases.
|
|
@@ -141,6 +141,10 @@ nav:
|
|
|
141
141
|
- User Guide:
|
|
142
142
|
- Overview: overview.md
|
|
143
143
|
- Architecture: architecture.md
|
|
144
|
+
- How-To:
|
|
145
|
+
- Alembic Migrations: how-to/alembic.md
|
|
146
|
+
- Multiple Databases: how-to/multiple-databases.md
|
|
147
|
+
- Testing: how-to/testing.md
|
|
144
148
|
- Reference:
|
|
145
149
|
- Configuration: reference/configuration.md
|
|
146
150
|
- Declarative Base: reference/declarative-base.md
|
|
@@ -150,4 +154,5 @@ nav:
|
|
|
150
154
|
- Tooling: development/project-tooling.md
|
|
151
155
|
- AI Coding Skills: skills.md
|
|
152
156
|
- FAQ: faq.md
|
|
157
|
+
- Troubleshooting: troubleshooting.md
|
|
153
158
|
- Changelog: CHANGELOG.md
|
|
@@ -42,12 +42,17 @@ dependencies = [
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
[project.optional-dependencies]
|
|
45
|
+
migrations = [
|
|
46
|
+
"alembic >= 1.13",
|
|
47
|
+
]
|
|
48
|
+
|
|
45
49
|
async = [
|
|
46
50
|
"asyncpg >= 0.29.0",
|
|
47
51
|
]
|
|
48
52
|
|
|
49
53
|
test = [
|
|
50
54
|
"pytest >= 8",
|
|
55
|
+
"alembic >= 1.13",
|
|
51
56
|
"pytest-asyncio >= 0.23.5",
|
|
52
57
|
"pytest-cov >= 5",
|
|
53
58
|
]
|
|
@@ -27,6 +27,7 @@ from .config import DatabaseConfigurer, DatabaseSettings
|
|
|
27
27
|
from .decorators import query, repository, transactional
|
|
28
28
|
from .factory import SqlAlchemyFactory
|
|
29
29
|
from .interceptor import TransactionalInterceptor
|
|
30
|
+
from .migrations import AlembicMigrator
|
|
30
31
|
from .paging import Page, PageRequest, Sort
|
|
31
32
|
from .repository_interceptor import RepositoryQueryInterceptor
|
|
32
33
|
from .session import SessionManager, get_session
|
|
@@ -41,6 +42,7 @@ __all__ = [
|
|
|
41
42
|
"get_session",
|
|
42
43
|
"TransactionalInterceptor",
|
|
43
44
|
"SqlAlchemyFactory",
|
|
45
|
+
"AlembicMigrator",
|
|
44
46
|
"AppBase",
|
|
45
47
|
"Mapped",
|
|
46
48
|
"mapped_column",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.5.0'
|
|
@@ -43,6 +43,7 @@ class DatabaseConfigurer(Protocol):
|
|
|
43
43
|
async def _create():
|
|
44
44
|
async with engine.begin() as conn:
|
|
45
45
|
await conn.run_sync(self.base.metadata.create_all)
|
|
46
|
+
await engine.dispose() # asyncpg pools are loop-bound
|
|
46
47
|
asyncio.run(_create())
|
|
47
48
|
"""
|
|
48
49
|
|
|
@@ -82,6 +83,10 @@ class DatabaseSettings:
|
|
|
82
83
|
pool_size: Connection pool size.
|
|
83
84
|
pool_pre_ping: Test connections before checkout.
|
|
84
85
|
pool_recycle: Recycle connections after this many seconds.
|
|
86
|
+
migrations_path: Alembic script directory (the one containing
|
|
87
|
+
``env.py``). Empty string (default) disables startup
|
|
88
|
+
migrations. Requires ``pico-sqlalchemy[migrations]``.
|
|
89
|
+
migrations_target: Alembic revision to upgrade to on startup.
|
|
85
90
|
"""
|
|
86
91
|
|
|
87
92
|
url: str = "sqlite+aiosqlite:///./app.db"
|
|
@@ -89,3 +94,5 @@ class DatabaseSettings:
|
|
|
89
94
|
pool_size: int = 5
|
|
90
95
|
pool_pre_ping: bool = True
|
|
91
96
|
pool_recycle: int = 3600
|
|
97
|
+
migrations_path: str = ""
|
|
98
|
+
migrations_target: str = "head"
|
|
@@ -10,6 +10,7 @@ These are auto-discovered when ``"pico_sqlalchemy"`` is listed in the
|
|
|
10
10
|
``modules`` argument of ``pico_ioc.init()``.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
from typing import List
|
|
14
15
|
|
|
15
16
|
from pico_ioc import component, configure, factory, provides
|
|
@@ -17,6 +18,8 @@ from pico_ioc import component, configure, factory, provides
|
|
|
17
18
|
from .config import DatabaseConfigurer, DatabaseSettings
|
|
18
19
|
from .session import SessionManager
|
|
19
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
def _priority_of(obj):
|
|
22
25
|
"""Return the ``priority`` of *obj* as an ``int``, defaulting to ``0``.
|
|
@@ -25,7 +28,12 @@ def _priority_of(obj):
|
|
|
25
28
|
"""
|
|
26
29
|
try:
|
|
27
30
|
return int(getattr(obj, "priority", 0))
|
|
28
|
-
except Exception:
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
logger.warning(
|
|
33
|
+
"DatabaseConfigurer %s has an unusable priority (%s); treating it as 0",
|
|
34
|
+
type(obj).__name__,
|
|
35
|
+
exc,
|
|
36
|
+
)
|
|
29
37
|
return 0
|
|
30
38
|
|
|
31
39
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Alembic migrations on startup.
|
|
2
|
+
|
|
3
|
+
Set ``database.migrations_path`` to your Alembic script directory (the
|
|
4
|
+
one containing ``env.py``) and the container runs ``upgrade head``
|
|
5
|
+
before any other ``DatabaseConfigurer`` (priority -100). Alembic is an
|
|
6
|
+
optional dependency: ``pip install pico-sqlalchemy[migrations]``.
|
|
7
|
+
|
|
8
|
+
The configured ``database.url`` is handed to Alembic verbatim as
|
|
9
|
+
``sqlalchemy.url`` — write ``env.py`` for the async URL (Alembic's
|
|
10
|
+
async template) or derive a sync engine from it there.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
from pico_ioc import component
|
|
16
|
+
|
|
17
|
+
from .config import DatabaseSettings
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@component
|
|
23
|
+
class AlembicMigrator:
|
|
24
|
+
priority = -100
|
|
25
|
+
|
|
26
|
+
def __init__(self, settings: DatabaseSettings):
|
|
27
|
+
self._settings = settings
|
|
28
|
+
|
|
29
|
+
def configure_database(self, engine) -> None:
|
|
30
|
+
path = self._settings.migrations_path
|
|
31
|
+
if not path:
|
|
32
|
+
return
|
|
33
|
+
try:
|
|
34
|
+
from alembic import command
|
|
35
|
+
from alembic.config import Config
|
|
36
|
+
except ImportError as exc:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"database.migrations_path is set but Alembic is not installed: pip install pico-sqlalchemy[migrations]"
|
|
39
|
+
) from exc
|
|
40
|
+
|
|
41
|
+
config = Config()
|
|
42
|
+
config.set_main_option("script_location", path)
|
|
43
|
+
config.set_main_option("sqlalchemy.url", self._settings.url)
|
|
44
|
+
target = self._settings.migrations_target
|
|
45
|
+
logger.info("running alembic upgrade %s from %s", target, path)
|
|
46
|
+
command.upgrade(config, target)
|
{pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/repository_interceptor.py
RENAMED
|
@@ -220,7 +220,10 @@ class RepositoryQueryInterceptor(MethodInterceptor):
|
|
|
220
220
|
meta = getattr(func, QUERY_META, None)
|
|
221
221
|
|
|
222
222
|
if meta is None:
|
|
223
|
-
|
|
223
|
+
result = call_next(ctx)
|
|
224
|
+
if inspect.isawaitable(result):
|
|
225
|
+
result = await result
|
|
226
|
+
return result
|
|
224
227
|
|
|
225
228
|
session = get_session(self.session_manager)
|
|
226
229
|
params = self._bind_params(func, ctx.args, ctx.kwargs)
|
|
@@ -234,13 +237,6 @@ class RepositoryQueryInterceptor(MethodInterceptor):
|
|
|
234
237
|
return await self._execute_expr(session, meta, params, entity)
|
|
235
238
|
raise RuntimeError(f"Unsupported query mode: {mode!r}")
|
|
236
239
|
|
|
237
|
-
async def _call_next_async(self, ctx: MethodCtx, call_next: Callable) -> Any:
|
|
238
|
-
"""Invoke the next interceptor or method, awaiting if needed."""
|
|
239
|
-
result = call_next(ctx)
|
|
240
|
-
if inspect.isawaitable(result):
|
|
241
|
-
result = await result
|
|
242
|
-
return result
|
|
243
|
-
|
|
244
240
|
def _bind_params(
|
|
245
241
|
self,
|
|
246
242
|
func: Callable[..., Any],
|
|
@@ -319,7 +315,8 @@ class RepositoryQueryInterceptor(MethodInterceptor):
|
|
|
319
315
|
unique = meta.get("unique", False)
|
|
320
316
|
paged = meta.get("paged", False)
|
|
321
317
|
|
|
322
|
-
|
|
318
|
+
if entity is None or not hasattr(entity, "__tablename__"):
|
|
319
|
+
raise RuntimeError("@query with expr requires @repository(entity=...) and an entity with __tablename__")
|
|
323
320
|
base_sql = self._build_base_sql(entity, expr)
|
|
324
321
|
|
|
325
322
|
page_req = _extract_page_request(params, paged)
|
|
@@ -331,16 +328,6 @@ class RepositoryQueryInterceptor(MethodInterceptor):
|
|
|
331
328
|
return await _execute_paginated_query(session, base_sql, params, page_req)
|
|
332
329
|
return await _execute_simple_query(session, base_sql, params, unique)
|
|
333
330
|
|
|
334
|
-
def _validate_entity(self, entity: Any) -> None:
|
|
335
|
-
"""Ensure *entity* is set and has a ``__tablename__`` attribute.
|
|
336
|
-
|
|
337
|
-
Raises:
|
|
338
|
-
RuntimeError: ``"@query with expr requires @repository(entity=...)
|
|
339
|
-
and an entity with __tablename__"``
|
|
340
|
-
"""
|
|
341
|
-
if entity is None or not hasattr(entity, "__tablename__"):
|
|
342
|
-
raise RuntimeError("@query with expr requires @repository(entity=...) and an entity with __tablename__")
|
|
343
|
-
|
|
344
331
|
def _build_base_sql(self, entity: Any, expr: str | None) -> str:
|
|
345
332
|
"""Build the base ``SELECT`` query from the entity and optional expression.
|
|
346
333
|
|
|
@@ -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
|
|
@@ -49,10 +49,13 @@ Description-Content-Type: text/markdown
|
|
|
49
49
|
License-File: LICENSE
|
|
50
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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
.coveragerc
|
|
2
|
+
.sonarcloud.properties
|
|
2
3
|
AGENTS.md
|
|
3
4
|
CHANGELOG.md
|
|
4
5
|
CLAUDE.md
|
|
@@ -19,7 +20,6 @@ tox.ini
|
|
|
19
20
|
.github/workflows/codeql.yml
|
|
20
21
|
.github/workflows/docs.yml
|
|
21
22
|
.github/workflows/publish-to-pypi.yml
|
|
22
|
-
.github/workflows/sync-keywords.yml
|
|
23
23
|
docs/CHANGELOG.md
|
|
24
24
|
docs/architecture.md
|
|
25
25
|
docs/faq.md
|
|
@@ -30,6 +30,7 @@ docs/overview.md
|
|
|
30
30
|
docs/quickstart.md
|
|
31
31
|
docs/requirements.txt
|
|
32
32
|
docs/skills.md
|
|
33
|
+
docs/troubleshooting.md
|
|
33
34
|
docs/development/project-tooling.md
|
|
34
35
|
docs/how-to/alembic.md
|
|
35
36
|
docs/how-to/multiple-databases.md
|
|
@@ -55,6 +56,7 @@ src/pico_sqlalchemy/config.py
|
|
|
55
56
|
src/pico_sqlalchemy/decorators.py
|
|
56
57
|
src/pico_sqlalchemy/factory.py
|
|
57
58
|
src/pico_sqlalchemy/interceptor.py
|
|
59
|
+
src/pico_sqlalchemy/migrations.py
|
|
58
60
|
src/pico_sqlalchemy/paging.py
|
|
59
61
|
src/pico_sqlalchemy/py.typed
|
|
60
62
|
src/pico_sqlalchemy/repository_interceptor.py
|
|
@@ -64,11 +66,14 @@ src/pico_sqlalchemy.egg-info/SOURCES.txt
|
|
|
64
66
|
src/pico_sqlalchemy.egg-info/dependency_links.txt
|
|
65
67
|
src/pico_sqlalchemy.egg-info/entry_points.txt
|
|
66
68
|
src/pico_sqlalchemy.egg-info/requires.txt
|
|
69
|
+
src/pico_sqlalchemy.egg-info/scm_file_list.json
|
|
70
|
+
src/pico_sqlalchemy.egg-info/scm_version.json
|
|
67
71
|
src/pico_sqlalchemy.egg-info/top_level.txt
|
|
68
72
|
tests/conftest.py
|
|
69
73
|
tests/test_coverage_boost_v2.py
|
|
70
74
|
tests/test_interceptor.py
|
|
71
75
|
tests/test_ioc_integration.py
|
|
76
|
+
tests/test_migrations.py
|
|
72
77
|
tests/test_pagination_sort.py
|
|
73
78
|
tests/test_repository_interceptor_coverage.py
|
|
74
79
|
tests/test_repository_query.py
|