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.
Files changed (91) hide show
  1. {pico_sqlalchemy-0.4.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.4.0/docs → pico_sqlalchemy-0.5.0}/CHANGELOG.md +8 -0
  8. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/PKG-INFO +4 -1
  9. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0/docs}/CHANGELOG.md +8 -0
  10. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/hooks.py +0 -1
  11. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/how-to/alembic.md +13 -0
  12. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/index.md +2 -0
  13. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/overview.md +1 -1
  14. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/configuration.md +2 -0
  15. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/repository.md +1 -1
  16. pico_sqlalchemy-0.5.0/docs/requirements.txt +7 -0
  17. pico_sqlalchemy-0.5.0/docs/troubleshooting.md +50 -0
  18. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/repositories.py +1 -0
  19. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/mkdocs.yml +5 -0
  20. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/pyproject.toml +5 -0
  21. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/__init__.py +2 -0
  22. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/_version.py +1 -0
  23. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/config.py +7 -0
  24. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/factory.py +9 -1
  25. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy/migrations.py +46 -0
  26. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/repository_interceptor.py +6 -19
  27. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/PKG-INFO +4 -1
  28. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/SOURCES.txt +6 -1
  29. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/requires.txt +4 -0
  30. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy.egg-info/scm_file_list.json +80 -0
  31. pico_sqlalchemy-0.5.0/src/pico_sqlalchemy.egg-info/scm_version.json +8 -0
  32. pico_sqlalchemy-0.5.0/tests/test_migrations.py +140 -0
  33. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tox.ini +2 -0
  34. pico_sqlalchemy-0.4.0/.github/workflows/ci.yml +0 -125
  35. pico_sqlalchemy-0.4.0/.github/workflows/codeql.yml +0 -28
  36. pico_sqlalchemy-0.4.0/.github/workflows/docs.yml +0 -104
  37. pico_sqlalchemy-0.4.0/.github/workflows/publish-to-pypi.yml +0 -48
  38. pico_sqlalchemy-0.4.0/.github/workflows/sync-keywords.yml +0 -80
  39. pico_sqlalchemy-0.4.0/docs/requirements.txt +0 -7
  40. pico_sqlalchemy-0.4.0/src/pico_sqlalchemy/_version.py +0 -1
  41. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.coveragerc +0 -0
  42. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  43. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  44. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/AGENTS.md +0 -0
  46. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/CLAUDE.md +0 -0
  47. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  48. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/CONTRIBUTING.md +0 -0
  49. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/LICENSE +0 -0
  50. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/MANIFEST.in +0 -0
  51. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/README.md +0 -0
  52. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/SECURITY.md +0 -0
  53. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/architecture.md +0 -0
  54. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/development/project-tooling.md +0 -0
  55. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/faq.md +0 -0
  56. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/how-to/multiple-databases.md +0 -0
  57. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/how-to/testing.md +0 -0
  58. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/javascripts/extra.js +0 -0
  59. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/migration.md +0 -0
  60. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/quickstart.md +0 -0
  61. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/declarative-base.md +0 -0
  62. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/reference/transactions.md +0 -0
  63. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/skills.md +0 -0
  64. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/docs/stylesheets/extra.css +0 -0
  65. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/README.md +0 -0
  66. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/__init__.py +0 -0
  67. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/main.py +0 -0
  68. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/models.py +0 -0
  69. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/app/services.py +0 -0
  70. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/config.yml +0 -0
  71. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/examples/crud-async/requirements.txt +0 -0
  72. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/setup.cfg +0 -0
  73. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/base.py +0 -0
  74. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/decorators.py +0 -0
  75. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/interceptor.py +0 -0
  76. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/paging.py +0 -0
  77. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/py.typed +0 -0
  78. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy/session.py +0 -0
  79. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/dependency_links.txt +0 -0
  80. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/entry_points.txt +0 -0
  81. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/src/pico_sqlalchemy.egg-info/top_level.txt +0 -0
  82. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/conftest.py +0 -0
  83. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_coverage_boost_v2.py +0 -0
  84. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_interceptor.py +0 -0
  85. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_ioc_integration.py +0 -0
  86. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_pagination_sort.py +0 -0
  87. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_interceptor_coverage.py +0 -0
  88. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_repository_query.py +0 -0
  89. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_session_propagation.py +0 -0
  90. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_transaction_manager.py +0 -0
  91. {pico_sqlalchemy-0.4.0 → pico_sqlalchemy-0.5.0}/tests/test_transaction_scope.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
@@ -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.4.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
@@ -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
-
@@ -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.
@@ -1,4 +1,4 @@
1
- # 🚀 Pico-SQLAlchemy: Async-Native ORM
1
+ # Pico-SQLAlchemy: Async-Native ORM
2
2
 
3
3
  `pico-sqlalchemy` is a thin integration layer that connects **Pico-IoC**’s inversion-of-control container with **SQLAlchemy**’s async session and transaction management.
4
4
 
@@ -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
- > **⚠️ Warning: Expression Mode Only**
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,7 @@
1
+ mkdocs>=1.6.1
2
+ mkdocs-material>=9.7.6
3
+ mkdocs-minify-plugin>=0.8.0
4
+ mkdocs-git-revision-date-localized-plugin>=1.5.3
5
+ mkdocs-awesome-pages-plugin>=2.10.1
6
+ pymdown-extensions>=11.0.1
7
+ mkdocstrings[python]>=1.0.5
@@ -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.
@@ -24,6 +24,7 @@ class UserRepository:
24
24
  @transactional(read_only=True)
25
25
  async def find_all(self, session) -> list[User]:
26
26
  from sqlalchemy import select
27
+
27
28
  result = await session.execute(select(User))
28
29
  return list(result.scalars().all())
29
30
 
@@ -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)
@@ -220,7 +220,10 @@ class RepositoryQueryInterceptor(MethodInterceptor):
220
220
  meta = getattr(func, QUERY_META, None)
221
221
 
222
222
  if meta is None:
223
- return await self._call_next_async(ctx, call_next)
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
- self._validate_entity(entity)
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.4.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