mgf-sqlalchemy 0.1.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.
- mgf_sqlalchemy-0.1.0/.gitignore +64 -0
- mgf_sqlalchemy-0.1.0/CHANGELOG.md +84 -0
- mgf_sqlalchemy-0.1.0/LICENSE +21 -0
- mgf_sqlalchemy-0.1.0/PKG-INFO +184 -0
- mgf_sqlalchemy-0.1.0/PUBLIC_API.md +81 -0
- mgf_sqlalchemy-0.1.0/README.md +145 -0
- mgf_sqlalchemy-0.1.0/docs/cutover/v0.1.0.md +316 -0
- mgf_sqlalchemy-0.1.0/docs/recipes/sqlalchemy.md +330 -0
- mgf_sqlalchemy-0.1.0/pyproject.toml +201 -0
- mgf_sqlalchemy-0.1.0/src/mgf/sqlalchemy/__init__.py +47 -0
- mgf_sqlalchemy-0.1.0/src/mgf/sqlalchemy/_engine.py +84 -0
- mgf_sqlalchemy-0.1.0/src/mgf/sqlalchemy/_session.py +105 -0
- mgf_sqlalchemy-0.1.0/src/mgf/sqlalchemy/py.typed +0 -0
- mgf_sqlalchemy-0.1.0/tests/unit/test_engine.py +50 -0
- mgf_sqlalchemy-0.1.0/tests/unit/test_session.py +146 -0
- mgf_sqlalchemy-0.1.0/uv.lock +1035 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# IDE
|
|
2
|
+
.idea/
|
|
3
|
+
.vscode/
|
|
4
|
+
*.swp
|
|
5
|
+
*.swo
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
*.so
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
lib/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
|
|
29
|
+
# Virtual environments
|
|
30
|
+
.venv/
|
|
31
|
+
venv/
|
|
32
|
+
env/
|
|
33
|
+
ENV/
|
|
34
|
+
|
|
35
|
+
# Testing and coverage
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.hypothesis/
|
|
38
|
+
.coverage
|
|
39
|
+
.coverage.*
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
|
|
46
|
+
# Type checking
|
|
47
|
+
.mypy_cache/
|
|
48
|
+
.pyre/
|
|
49
|
+
.pytype/
|
|
50
|
+
|
|
51
|
+
# Ruff
|
|
52
|
+
.ruff_cache/
|
|
53
|
+
|
|
54
|
+
# Packaging
|
|
55
|
+
MANIFEST
|
|
56
|
+
|
|
57
|
+
# Claude Code — keep settings.json (committed for the team), ignore
|
|
58
|
+
# per-machine local overrides + runtime locks/state.
|
|
59
|
+
.claude/settings.local.json
|
|
60
|
+
.claude/*.lock
|
|
61
|
+
.claude/scheduled_tasks*
|
|
62
|
+
|
|
63
|
+
# Maintainer planning notes (per-machine; never commit)
|
|
64
|
+
my_stuff/
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mgf-sqlalchemy` are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
6
|
+
the project follows [SemVer](https://semver.org/spec/v2.0.0.html). Per
|
|
7
|
+
the 0.x window ([SemVer 0.x](https://semver.org/#spec-item-4) and rule
|
|
8
|
+
**AP-03** from `mgf-common/docs/standards/API_DESIGN.md`), MINOR
|
|
9
|
+
releases MAY break the public API. Pin tightly:
|
|
10
|
+
|
|
11
|
+
```toml
|
|
12
|
+
mgf-sqlalchemy = ">=0.X.0,<0.Y" # one minor at a time
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The release-engineering discipline that produces this changelog is in
|
|
16
|
+
[`mgf-common/docs/standards/RELEASING.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/standards/RELEASING.md).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [Unreleased]
|
|
21
|
+
|
|
22
|
+
Nothing yet.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [0.1.0] — 2026-05-10
|
|
27
|
+
|
|
28
|
+
PyPI: <https://pypi.org/project/mgf-sqlalchemy/0.1.0/> · Tag: `v0.1.0`
|
|
29
|
+
|
|
30
|
+
> **Maiden voyage** — extracted from `mgf-common` v0.29.0 per the
|
|
31
|
+
> federation split plan ([mgf-common/docs/release/federation_roadmap.md](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md)).
|
|
32
|
+
> Cutover guide: [`docs/cutover/v0.1.0.md`](docs/cutover/v0.1.0.md).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`mgf.sqlalchemy.create_engine`** — async SQLAlchemy engine
|
|
37
|
+
factory wrapping `sqlalchemy.ext.asyncio.create_async_engine` with
|
|
38
|
+
production-leaning defaults: `pool_pre_ping=True` (catches stale
|
|
39
|
+
connections after DB restart), `pool_recycle=3600` (avoids MySQL
|
|
40
|
+
`wait_timeout` surprise), conservative `pool_size`/`max_overflow`.
|
|
41
|
+
SQLite-aware: detects `sqlite://` / `sqlite+` URLs and omits pool
|
|
42
|
+
kwargs that `StaticPool` / `NullPool` reject. `**extra` forwarded
|
|
43
|
+
to `create_async_engine` for rare options.
|
|
44
|
+
- **`mgf.sqlalchemy.create_sessionmaker`** — async sessionmaker
|
|
45
|
+
factory wrapping `async_sessionmaker` with SQLAlchemy 2's
|
|
46
|
+
recommended `expire_on_commit=False` default for async sessions.
|
|
47
|
+
`expire_on_commit=True` opts back into the legacy synchronous
|
|
48
|
+
semantics.
|
|
49
|
+
- **`mgf.sqlalchemy.tenant_session`** — async context manager that
|
|
50
|
+
runs `SET LOCAL app.current_tenant = '<uuid>'` for Postgres
|
|
51
|
+
RLS-based multi-tenancy. The `LOCAL` qualifier scopes to the
|
|
52
|
+
current transaction (auto-resets on commit/rollback) so
|
|
53
|
+
cross-tenant leakage between requests is impossible at the SQL
|
|
54
|
+
layer. `tenant_id` is UUID-validated before interpolation
|
|
55
|
+
(Postgres `SET LOCAL` doesn't accept bind params, so literal
|
|
56
|
+
interpolation is the only path; UUID validation makes it safe).
|
|
57
|
+
|
|
58
|
+
### Removed (BREAKING vs the pre-extraction `mgf.common.db` shape)
|
|
59
|
+
|
|
60
|
+
- **`get_session` and `setup_db`** are NOT in this sibling. They
|
|
61
|
+
were FastAPI-Depends and FastAPI-lifespan shaped respectively in
|
|
62
|
+
mgf-common — pulling Starlette/FastAPI into a "framework-agnostic
|
|
63
|
+
SQLAlchemy" sibling would violate the closed-box rule. They moved
|
|
64
|
+
to `mgf.fastapi.db.get_session` / `mgf.fastapi.db.setup_db` in
|
|
65
|
+
mgf-fastapi v0.2.0 (gated behind the `mgf-fastapi[sqlalchemy]`
|
|
66
|
+
optional extra). Consumers needing them install both
|
|
67
|
+
`mgf-sqlalchemy` and `mgf-fastapi[sqlalchemy]`.
|
|
68
|
+
|
|
69
|
+
### Engineering
|
|
70
|
+
|
|
71
|
+
- 11 tests carried over from `mgf-common/tests/unit/db/test_engine.py`
|
|
72
|
+
+ the non-FastAPI portions of `test_session.py` — every
|
|
73
|
+
pre-extraction failure mode for the helpers in this sibling stays
|
|
74
|
+
green at parity. Coverage threshold ≥80% (matches mgf-common's
|
|
75
|
+
standard).
|
|
76
|
+
- Imports rewrite from `mgf.common.db.*` to `mgf.sqlalchemy.*`. No
|
|
77
|
+
private-import leaks; the sibling reaches into mgf-common only
|
|
78
|
+
through public names.
|
|
79
|
+
- `mgf-common>=0.30,<0.31` runtime dependency. AP-03 0.x window pin
|
|
80
|
+
discipline applies — bump in lock-step with mgf-common's next
|
|
81
|
+
minor.
|
|
82
|
+
- PEP 420 namespace package (no top-level `mgf/__init__.py`); the
|
|
83
|
+
wheel ships `src/mgf/` as-is so `mgf.sqlalchemy`, `mgf.alembic`,
|
|
84
|
+
`mgf.fastapi`, `mgf.http`, and `mgf.common` coexist as siblings.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bassam Alsanie and mgf-common contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mgf-sqlalchemy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async SQLAlchemy helpers for mgf-common consumers — typed engine factory, sessionmaker, Postgres RLS tenant-scoping. Sibling of mgf-common under the mgf.* namespace.
|
|
5
|
+
Project-URL: Homepage, https://codeberg.org/magogi-admin/mgf-sqlalchemy
|
|
6
|
+
Project-URL: Issues, https://codeberg.org/magogi-admin/mgf-sqlalchemy/issues
|
|
7
|
+
Project-URL: Changelog, https://codeberg.org/magogi-admin/mgf-sqlalchemy/src/branch/main/CHANGELOG.md
|
|
8
|
+
Author: Bassam Alsanie, mgf-sqlalchemy contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: async,asyncpg,engine,multi-tenant,rls,sqlalchemy
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: mgf-common<0.31,>=0.30
|
|
27
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
|
|
30
|
+
Requires-Dist: import-linter>=2.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: aiosqlite>=0.20; extra == 'test'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# `mgf-sqlalchemy` — async SQLAlchemy helpers for mgf-common consumers
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/mgf-sqlalchemy/)
|
|
43
|
+
[](https://pypi.org/project/mgf-sqlalchemy/)
|
|
44
|
+
|
|
45
|
+
> **Sibling of [`mgf-common`](https://pypi.org/project/mgf-common/)
|
|
46
|
+
> under the `mgf.*` namespace.** Houses the async-SQLAlchemy helpers
|
|
47
|
+
> that previously lived under `mgf.common.db.*` — extracted at
|
|
48
|
+
> mgf-common v0.30 / mgf-sqlalchemy v0.1 per the
|
|
49
|
+
> [federation split plan](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md).
|
|
50
|
+
|
|
51
|
+
## What this provides
|
|
52
|
+
|
|
53
|
+
| Submodule | What |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `mgf.sqlalchemy` | `create_engine` — async SQLAlchemy engine factory with production-leaning pool defaults (`pool_pre_ping`, `pool_recycle`, sane `pool_size`/`max_overflow`). SQLite-aware (skips pool kwargs that StaticPool/NullPool reject). `create_sessionmaker` — async sessionmaker factory with SQLAlchemy 2's recommended `expire_on_commit=False` default. `tenant_session` — context manager for Postgres RLS multi-tenancy (`SET LOCAL app.current_tenant = '<uuid>'`); UUID-validated to prevent SQL injection. |
|
|
56
|
+
|
|
57
|
+
**FastAPI helpers live elsewhere.** The `get_session`
|
|
58
|
+
FastAPI-Depends generator and the `setup_db` lifespan helper that
|
|
59
|
+
previously co-located with these in mgf-common moved to
|
|
60
|
+
[`mgf.fastapi.db`](https://pypi.org/project/mgf-fastapi/) in
|
|
61
|
+
mgf-fastapi v0.2.0 (which depends on mgf-sqlalchemy via its
|
|
62
|
+
`[sqlalchemy]` extra). This sibling stays framework-agnostic — no
|
|
63
|
+
Starlette / no FastAPI in the dependency graph.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install mgf-sqlalchemy
|
|
69
|
+
# Or with the test extra (aiosqlite for in-memory SQLite tests):
|
|
70
|
+
pip install 'mgf-sqlalchemy[test]'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Pulls in `mgf-common` + `sqlalchemy[asyncio]` automatically. Production
|
|
74
|
+
consumers also need an async driver of their own (asyncpg / aiomysql /
|
|
75
|
+
asyncmy); we don't pin one — the consumer picks based on their
|
|
76
|
+
database.
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
from mgf.sqlalchemy import create_engine, create_sessionmaker
|
|
83
|
+
|
|
84
|
+
async def main() -> None:
|
|
85
|
+
engine = create_engine(
|
|
86
|
+
"postgresql+asyncpg://user:pass@localhost/myapp",
|
|
87
|
+
echo=False,
|
|
88
|
+
)
|
|
89
|
+
sessionmaker = create_sessionmaker(engine)
|
|
90
|
+
try:
|
|
91
|
+
async with sessionmaker() as session:
|
|
92
|
+
from sqlalchemy import text
|
|
93
|
+
row = (await session.execute(text("SELECT 1"))).scalar_one()
|
|
94
|
+
print(row)
|
|
95
|
+
finally:
|
|
96
|
+
await engine.dispose()
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Postgres RLS multi-tenancy
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from uuid import UUID
|
|
105
|
+
from mgf.sqlalchemy import tenant_session
|
|
106
|
+
|
|
107
|
+
tenant_id = UUID("550e8400-e29b-41d4-a716-446655440000")
|
|
108
|
+
|
|
109
|
+
async with sessionmaker() as session:
|
|
110
|
+
async with tenant_session(session, tenant_id) as scoped:
|
|
111
|
+
# Every query on `scoped` (same session, just tenant-scoped)
|
|
112
|
+
# gets `app.current_tenant` set in the current transaction.
|
|
113
|
+
# RLS policies in your schema can read from
|
|
114
|
+
# `current_setting('app.current_tenant')`.
|
|
115
|
+
rows = await scoped.execute(text("SELECT ..."))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## FastAPI integration
|
|
119
|
+
|
|
120
|
+
The FastAPI-shaped helpers (request-scoped session injection +
|
|
121
|
+
lifespan) live in mgf-fastapi:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# pyproject.toml
|
|
125
|
+
dependencies = [
|
|
126
|
+
"mgf-common>=0.30,<0.31",
|
|
127
|
+
"mgf-sqlalchemy>=0.1,<0.2",
|
|
128
|
+
"mgf-fastapi[sqlalchemy]>=0.2,<0.3",
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from typing import Annotated
|
|
134
|
+
from contextlib import asynccontextmanager
|
|
135
|
+
from fastapi import FastAPI, Depends
|
|
136
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
137
|
+
from mgf.fastapi.db import get_session, setup_db
|
|
138
|
+
|
|
139
|
+
@asynccontextmanager
|
|
140
|
+
async def lifespan(app: FastAPI):
|
|
141
|
+
async with setup_db(app, database_url="postgresql+asyncpg://..."):
|
|
142
|
+
yield
|
|
143
|
+
|
|
144
|
+
app = FastAPI(lifespan=lifespan)
|
|
145
|
+
|
|
146
|
+
@app.get("/users")
|
|
147
|
+
async def list_users(
|
|
148
|
+
session: Annotated[AsyncSession, Depends(get_session)],
|
|
149
|
+
) -> list[dict]:
|
|
150
|
+
...
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
- [`docs/recipes/sqlalchemy.md`](docs/recipes/sqlalchemy.md) — full async-SQLAlchemy walkthrough.
|
|
156
|
+
- [`docs/cutover/v0.1.0.md`](docs/cutover/v0.1.0.md) — maiden voyage migration story (the v0.30 split + the get_session/setup_db relocation to mgf-fastapi).
|
|
157
|
+
- [`PUBLIC_API.md`](PUBLIC_API.md) — full public surface contract.
|
|
158
|
+
- [`CHANGELOG.md`](CHANGELOG.md) — release history.
|
|
159
|
+
|
|
160
|
+
For the federation-wide engineering standards (DESIGN_PRINCIPLES,
|
|
161
|
+
ERROR_HANDLING, SECURITY, etc.) see
|
|
162
|
+
[`mgf-common/docs/standards/`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/standards/).
|
|
163
|
+
This sibling inherits them by reference; the standards
|
|
164
|
+
source-of-truth lives in mgf-common.
|
|
165
|
+
|
|
166
|
+
## Status
|
|
167
|
+
|
|
168
|
+
🚧 **Experimental** — every public name is `experimental` per AP-09.
|
|
169
|
+
Promotion to `stable` happens release-by-release as consumer feedback
|
|
170
|
+
in [`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/FEEDBACK.md)
|
|
171
|
+
converges. The 0.x window applies. Pin tightly:
|
|
172
|
+
`mgf-sqlalchemy = ">=0.X.0,<0.Y"`.
|
|
173
|
+
|
|
174
|
+
## Cross-references
|
|
175
|
+
|
|
176
|
+
- **Filing process for sharp edges**: open an entry on
|
|
177
|
+
[`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/FEEDBACK.md)
|
|
178
|
+
with `[mgf-sqlalchemy]` prefix, OR file directly on this repo's
|
|
179
|
+
Issues → maintainer mirrors into the canonical FEEDBACK.md.
|
|
180
|
+
- **Federation pattern**:
|
|
181
|
+
[`mgf-common/docs/design/federation.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/design/federation.md).
|
|
182
|
+
- **The split that created this sibling**:
|
|
183
|
+
[`mgf-common/docs/release/federation_roadmap.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md).
|
|
184
|
+
- **Companion sibling**: [`mgf-alembic`](https://pypi.org/project/mgf-alembic/) — async-aware alembic env.py helper (paired ship at v0.30; depends on this sibling).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Public API — `mgf-sqlalchemy`
|
|
2
|
+
|
|
3
|
+
> ⚠️ **This file documents `master` HEAD.** Names listed here may
|
|
4
|
+
> be unreleased. To learn what your installed wheel actually
|
|
5
|
+
> ships, run `pip show mgf-sqlalchemy` and compare against the git
|
|
6
|
+
> tag matching the published version.
|
|
7
|
+
|
|
8
|
+
This document is the **contract list** for `mgf-sqlalchemy`. Every
|
|
9
|
+
name listed below is a public name that consumers MAY depend on.
|
|
10
|
+
|
|
11
|
+
The contract terms are defined in
|
|
12
|
+
[`mgf-common/docs/standards/API_DESIGN.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/standards/API_DESIGN.md). In
|
|
13
|
+
short:
|
|
14
|
+
|
|
15
|
+
- **Stability tiers:**
|
|
16
|
+
- `experimental` — MAY change shape in any MINOR release
|
|
17
|
+
(the 0.x window keeps everything experimental in practice).
|
|
18
|
+
- `stable` — follows the deprecation cycle; removal only in MAJOR.
|
|
19
|
+
- `deprecated` — slated for removal; emits `DeprecationWarning`.
|
|
20
|
+
- **Names not listed below are private.** Anything starting with
|
|
21
|
+
`_` (underscore-prefixed module names like `_engine.py`) is
|
|
22
|
+
internal and MAY change without notice.
|
|
23
|
+
- **The 0.x window applies.** Per [SemVer](https://semver.org/) 0.x
|
|
24
|
+
semantics, MINOR releases MAY break the public API. Pin tightly:
|
|
25
|
+
`mgf-sqlalchemy = ">=0.X.0,<0.Y"`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## `mgf.sqlalchemy`
|
|
30
|
+
|
|
31
|
+
Top-level async SQLAlchemy helpers. Closed-box: depends on
|
|
32
|
+
`mgf-common` + `sqlalchemy[asyncio]` only — **no FastAPI / Starlette /
|
|
33
|
+
Django** in the runtime graph.
|
|
34
|
+
|
|
35
|
+
| Name | Tier | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `create_engine` | experimental | Async SQLAlchemy engine factory. Wraps `sqlalchemy.ext.asyncio.create_async_engine` with production-leaning pool defaults (`pool_pre_ping=True`, `pool_recycle=3600`, conservative `pool_size`/`max_overflow`). SQLite-aware (skips pool kwargs that StaticPool/NullPool reject). `**extra` forwarded for rare options like `connect_args`, `isolation_level`. Returns an unconnected `AsyncEngine`. |
|
|
38
|
+
| `create_sessionmaker` | experimental | Async sessionmaker factory. Wraps `async_sessionmaker` with SQLAlchemy 2's recommended `expire_on_commit=False` default. `expire_on_commit=True` opts back into the legacy synchronous semantics. |
|
|
39
|
+
| `tenant_session` | experimental | Async context manager for Postgres RLS multi-tenancy. Runs `SET LOCAL app.current_tenant = '<uuid>'` on the supplied session; the `LOCAL` qualifier scopes the setting to the current transaction (auto-resets on commit/rollback). `tenant_id` accepts a `UUID` object or a UUID-shaped string; non-UUID input raises `ValueError` before any SQL is emitted. SQLite raises at the SQL layer (no GUC support); Postgres needs the GUC declared either in `postgresql.conf` or via a migration. |
|
|
40
|
+
|
|
41
|
+
### Behaviours guaranteed at the public surface
|
|
42
|
+
|
|
43
|
+
- `create_engine` accepts any SQLAlchemy URL and never connects
|
|
44
|
+
eagerly — the first connection attempt happens lazily on the
|
|
45
|
+
first `await engine.connect()` or sessionmaker call.
|
|
46
|
+
- `tenant_session` validates `tenant_id` is UUID-shaped BEFORE
|
|
47
|
+
interpolating into the `SET LOCAL` SQL. Non-UUID input raises
|
|
48
|
+
`ValueError`; the SQL never runs.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What stays in `mgf-common`, NOT here
|
|
53
|
+
|
|
54
|
+
- **`AppError`, `OperationError`, `AppConfigError`** and the rest
|
|
55
|
+
of the typed exception hierarchy — `mgf.common.exceptions`.
|
|
56
|
+
- **`bootstrap`, `app_name`, `app_version`, `current_context`** —
|
|
57
|
+
`mgf.common`.
|
|
58
|
+
|
|
59
|
+
`mgf-sqlalchemy` reaches into none of mgf-common's private modules.
|
|
60
|
+
The runtime dep is `mgf-common>=0.30,<0.31`.
|
|
61
|
+
|
|
62
|
+
## What lives in `mgf-fastapi`, NOT here
|
|
63
|
+
|
|
64
|
+
- **`get_session`** — FastAPI-Depends-compatible session generator.
|
|
65
|
+
Yields one `AsyncSession` per request from
|
|
66
|
+
`request.app.state.mgf_db_sessionmaker`. Lives in
|
|
67
|
+
`mgf.fastapi.db.get_session` (mgf-fastapi v0.2+).
|
|
68
|
+
- **`setup_db`** — FastAPI lifespan context manager that creates
|
|
69
|
+
the engine + sessionmaker, stashes them on `app.state`, and
|
|
70
|
+
disposes the engine cleanly at lifespan exit. Lives in
|
|
71
|
+
`mgf.fastapi.db.setup_db` (mgf-fastapi v0.2+).
|
|
72
|
+
|
|
73
|
+
Install both siblings to get the FastAPI-shaped helpers:
|
|
74
|
+
|
|
75
|
+
```toml
|
|
76
|
+
dependencies = [
|
|
77
|
+
"mgf-common>=0.30,<0.31",
|
|
78
|
+
"mgf-sqlalchemy>=0.1,<0.2",
|
|
79
|
+
"mgf-fastapi[sqlalchemy]>=0.2,<0.3",
|
|
80
|
+
]
|
|
81
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# `mgf-sqlalchemy` — async SQLAlchemy helpers for mgf-common consumers
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/mgf-sqlalchemy/)
|
|
4
|
+
[](https://pypi.org/project/mgf-sqlalchemy/)
|
|
5
|
+
|
|
6
|
+
> **Sibling of [`mgf-common`](https://pypi.org/project/mgf-common/)
|
|
7
|
+
> under the `mgf.*` namespace.** Houses the async-SQLAlchemy helpers
|
|
8
|
+
> that previously lived under `mgf.common.db.*` — extracted at
|
|
9
|
+
> mgf-common v0.30 / mgf-sqlalchemy v0.1 per the
|
|
10
|
+
> [federation split plan](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md).
|
|
11
|
+
|
|
12
|
+
## What this provides
|
|
13
|
+
|
|
14
|
+
| Submodule | What |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `mgf.sqlalchemy` | `create_engine` — async SQLAlchemy engine factory with production-leaning pool defaults (`pool_pre_ping`, `pool_recycle`, sane `pool_size`/`max_overflow`). SQLite-aware (skips pool kwargs that StaticPool/NullPool reject). `create_sessionmaker` — async sessionmaker factory with SQLAlchemy 2's recommended `expire_on_commit=False` default. `tenant_session` — context manager for Postgres RLS multi-tenancy (`SET LOCAL app.current_tenant = '<uuid>'`); UUID-validated to prevent SQL injection. |
|
|
17
|
+
|
|
18
|
+
**FastAPI helpers live elsewhere.** The `get_session`
|
|
19
|
+
FastAPI-Depends generator and the `setup_db` lifespan helper that
|
|
20
|
+
previously co-located with these in mgf-common moved to
|
|
21
|
+
[`mgf.fastapi.db`](https://pypi.org/project/mgf-fastapi/) in
|
|
22
|
+
mgf-fastapi v0.2.0 (which depends on mgf-sqlalchemy via its
|
|
23
|
+
`[sqlalchemy]` extra). This sibling stays framework-agnostic — no
|
|
24
|
+
Starlette / no FastAPI in the dependency graph.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install mgf-sqlalchemy
|
|
30
|
+
# Or with the test extra (aiosqlite for in-memory SQLite tests):
|
|
31
|
+
pip install 'mgf-sqlalchemy[test]'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pulls in `mgf-common` + `sqlalchemy[asyncio]` automatically. Production
|
|
35
|
+
consumers also need an async driver of their own (asyncpg / aiomysql /
|
|
36
|
+
asyncmy); we don't pin one — the consumer picks based on their
|
|
37
|
+
database.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import asyncio
|
|
43
|
+
from mgf.sqlalchemy import create_engine, create_sessionmaker
|
|
44
|
+
|
|
45
|
+
async def main() -> None:
|
|
46
|
+
engine = create_engine(
|
|
47
|
+
"postgresql+asyncpg://user:pass@localhost/myapp",
|
|
48
|
+
echo=False,
|
|
49
|
+
)
|
|
50
|
+
sessionmaker = create_sessionmaker(engine)
|
|
51
|
+
try:
|
|
52
|
+
async with sessionmaker() as session:
|
|
53
|
+
from sqlalchemy import text
|
|
54
|
+
row = (await session.execute(text("SELECT 1"))).scalar_one()
|
|
55
|
+
print(row)
|
|
56
|
+
finally:
|
|
57
|
+
await engine.dispose()
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Postgres RLS multi-tenancy
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from uuid import UUID
|
|
66
|
+
from mgf.sqlalchemy import tenant_session
|
|
67
|
+
|
|
68
|
+
tenant_id = UUID("550e8400-e29b-41d4-a716-446655440000")
|
|
69
|
+
|
|
70
|
+
async with sessionmaker() as session:
|
|
71
|
+
async with tenant_session(session, tenant_id) as scoped:
|
|
72
|
+
# Every query on `scoped` (same session, just tenant-scoped)
|
|
73
|
+
# gets `app.current_tenant` set in the current transaction.
|
|
74
|
+
# RLS policies in your schema can read from
|
|
75
|
+
# `current_setting('app.current_tenant')`.
|
|
76
|
+
rows = await scoped.execute(text("SELECT ..."))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## FastAPI integration
|
|
80
|
+
|
|
81
|
+
The FastAPI-shaped helpers (request-scoped session injection +
|
|
82
|
+
lifespan) live in mgf-fastapi:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# pyproject.toml
|
|
86
|
+
dependencies = [
|
|
87
|
+
"mgf-common>=0.30,<0.31",
|
|
88
|
+
"mgf-sqlalchemy>=0.1,<0.2",
|
|
89
|
+
"mgf-fastapi[sqlalchemy]>=0.2,<0.3",
|
|
90
|
+
]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from typing import Annotated
|
|
95
|
+
from contextlib import asynccontextmanager
|
|
96
|
+
from fastapi import FastAPI, Depends
|
|
97
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
98
|
+
from mgf.fastapi.db import get_session, setup_db
|
|
99
|
+
|
|
100
|
+
@asynccontextmanager
|
|
101
|
+
async def lifespan(app: FastAPI):
|
|
102
|
+
async with setup_db(app, database_url="postgresql+asyncpg://..."):
|
|
103
|
+
yield
|
|
104
|
+
|
|
105
|
+
app = FastAPI(lifespan=lifespan)
|
|
106
|
+
|
|
107
|
+
@app.get("/users")
|
|
108
|
+
async def list_users(
|
|
109
|
+
session: Annotated[AsyncSession, Depends(get_session)],
|
|
110
|
+
) -> list[dict]:
|
|
111
|
+
...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [`docs/recipes/sqlalchemy.md`](docs/recipes/sqlalchemy.md) — full async-SQLAlchemy walkthrough.
|
|
117
|
+
- [`docs/cutover/v0.1.0.md`](docs/cutover/v0.1.0.md) — maiden voyage migration story (the v0.30 split + the get_session/setup_db relocation to mgf-fastapi).
|
|
118
|
+
- [`PUBLIC_API.md`](PUBLIC_API.md) — full public surface contract.
|
|
119
|
+
- [`CHANGELOG.md`](CHANGELOG.md) — release history.
|
|
120
|
+
|
|
121
|
+
For the federation-wide engineering standards (DESIGN_PRINCIPLES,
|
|
122
|
+
ERROR_HANDLING, SECURITY, etc.) see
|
|
123
|
+
[`mgf-common/docs/standards/`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/standards/).
|
|
124
|
+
This sibling inherits them by reference; the standards
|
|
125
|
+
source-of-truth lives in mgf-common.
|
|
126
|
+
|
|
127
|
+
## Status
|
|
128
|
+
|
|
129
|
+
🚧 **Experimental** — every public name is `experimental` per AP-09.
|
|
130
|
+
Promotion to `stable` happens release-by-release as consumer feedback
|
|
131
|
+
in [`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/FEEDBACK.md)
|
|
132
|
+
converges. The 0.x window applies. Pin tightly:
|
|
133
|
+
`mgf-sqlalchemy = ">=0.X.0,<0.Y"`.
|
|
134
|
+
|
|
135
|
+
## Cross-references
|
|
136
|
+
|
|
137
|
+
- **Filing process for sharp edges**: open an entry on
|
|
138
|
+
[`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/FEEDBACK.md)
|
|
139
|
+
with `[mgf-sqlalchemy]` prefix, OR file directly on this repo's
|
|
140
|
+
Issues → maintainer mirrors into the canonical FEEDBACK.md.
|
|
141
|
+
- **Federation pattern**:
|
|
142
|
+
[`mgf-common/docs/design/federation.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/design/federation.md).
|
|
143
|
+
- **The split that created this sibling**:
|
|
144
|
+
[`mgf-common/docs/release/federation_roadmap.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md).
|
|
145
|
+
- **Companion sibling**: [`mgf-alembic`](https://pypi.org/project/mgf-alembic/) — async-aware alembic env.py helper (paired ship at v0.30; depends on this sibling).
|