mgf-sqlalchemy 0.1.0__py3-none-any.whl
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/__init__.py +47 -0
- mgf/sqlalchemy/_engine.py +84 -0
- mgf/sqlalchemy/_session.py +105 -0
- mgf/sqlalchemy/py.typed +0 -0
- mgf_sqlalchemy-0.1.0.dist-info/METADATA +184 -0
- mgf_sqlalchemy-0.1.0.dist-info/RECORD +8 -0
- mgf_sqlalchemy-0.1.0.dist-info/WHEEL +4 -0
- mgf_sqlalchemy-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""``mgf.sqlalchemy`` — async SQLAlchemy helpers for mgf-common consumers.
|
|
2
|
+
|
|
3
|
+
Sibling of :mod:`mgf.common` under the ``mgf.*`` namespace. Houses the
|
|
4
|
+
async-engine factory + sessionmaker + tenant-RLS helper that previously
|
|
5
|
+
lived under ``mgf.common.db.*`` — extracted at mgf-common v0.30 /
|
|
6
|
+
mgf-sqlalchemy v0.1 per the federation split plan.
|
|
7
|
+
|
|
8
|
+
Every async-SQLAlchemy consumer needs the same three things:
|
|
9
|
+
|
|
10
|
+
- An :class:`AsyncEngine` factory with sane defaults (pool sizing,
|
|
11
|
+
``pool_pre_ping``, ``pool_recycle``, ``echo`` from settings).
|
|
12
|
+
- An :func:`async_sessionmaker` factory wired to that engine.
|
|
13
|
+
- A per-request tenant-scoping context manager
|
|
14
|
+
(``SET LOCAL app.current_tenant = '<uuid>'`` for Postgres RLS).
|
|
15
|
+
|
|
16
|
+
This sibling is **framework-agnostic**: no FastAPI, no Starlette, no
|
|
17
|
+
Django imports. The FastAPI-Depends-shaped ``get_session`` and the
|
|
18
|
+
``setup_db`` lifespan helper that previously lived under
|
|
19
|
+
``mgf.common.db`` were moved to the :mod:`mgf.fastapi.db` submodule
|
|
20
|
+
in mgf-fastapi v0.2.0 (which depends on mgf-sqlalchemy via the
|
|
21
|
+
``[sqlalchemy]`` extra). See ``mgf-sqlalchemy/docs/cutover/v0.1.0.md``
|
|
22
|
+
for the full migration map.
|
|
23
|
+
|
|
24
|
+
Install: ``pip install mgf-sqlalchemy`` (pulls
|
|
25
|
+
``sqlalchemy[asyncio]>=2.0``). For tests against an in-memory SQLite
|
|
26
|
+
database: ``pip install 'mgf-sqlalchemy[test]'`` (adds ``aiosqlite``).
|
|
27
|
+
|
|
28
|
+
The integration is **adapter-shaped, not framework-shaped**. Nothing
|
|
29
|
+
here re-exports SQLAlchemy primitives or hides them. The consumer's
|
|
30
|
+
ORM is still vanilla SQLAlchemy 2; these helpers plug into the seams
|
|
31
|
+
(engine factory, sessionmaker).
|
|
32
|
+
|
|
33
|
+
Closed-box invariant: ``mgf-sqlalchemy`` depends on ``mgf-common`` +
|
|
34
|
+
``sqlalchemy`` only. Consumers MUST import from ``mgf.sqlalchemy``,
|
|
35
|
+
never reach into ``mgf.sqlalchemy._engine`` / ``mgf.sqlalchemy._session``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from mgf.sqlalchemy._engine import create_engine
|
|
41
|
+
from mgf.sqlalchemy._session import create_sessionmaker, tenant_session
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"create_engine",
|
|
45
|
+
"create_sessionmaker",
|
|
46
|
+
"tenant_session",
|
|
47
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Async SQLAlchemy engine factory.
|
|
2
|
+
|
|
3
|
+
Wraps :func:`sqlalchemy.ext.asyncio.create_async_engine` with
|
|
4
|
+
production-leaning defaults:
|
|
5
|
+
|
|
6
|
+
- ``pool_pre_ping=True`` — every checkout pings the connection.
|
|
7
|
+
Costs one round-trip per checkout but catches stale connections
|
|
8
|
+
after DB restart / firewall reset / etc.
|
|
9
|
+
- ``pool_recycle=3600`` — recycle connections older than an hour.
|
|
10
|
+
Avoids the MySQL "wait_timeout" surprise + Postgres connection
|
|
11
|
+
leaks.
|
|
12
|
+
- ``pool_size`` / ``max_overflow`` set to conservative defaults
|
|
13
|
+
that work for SQLite + scale to Postgres / MySQL.
|
|
14
|
+
|
|
15
|
+
Consumers override any default via kwargs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
23
|
+
|
|
24
|
+
# Production-leaning defaults. Override per-consumer as needed.
|
|
25
|
+
_DEFAULT_POOL_PRE_PING: bool = True
|
|
26
|
+
_DEFAULT_POOL_RECYCLE_SECONDS: int = 3600 # 1 hour
|
|
27
|
+
_DEFAULT_POOL_SIZE: int = 5
|
|
28
|
+
_DEFAULT_MAX_OVERFLOW: int = 10
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_engine(
|
|
32
|
+
database_url: str,
|
|
33
|
+
*,
|
|
34
|
+
echo: bool = False,
|
|
35
|
+
pool_pre_ping: bool = _DEFAULT_POOL_PRE_PING,
|
|
36
|
+
pool_recycle: int = _DEFAULT_POOL_RECYCLE_SECONDS,
|
|
37
|
+
pool_size: int = _DEFAULT_POOL_SIZE,
|
|
38
|
+
max_overflow: int = _DEFAULT_MAX_OVERFLOW,
|
|
39
|
+
**extra: Any,
|
|
40
|
+
) -> AsyncEngine:
|
|
41
|
+
"""Build an async SQLAlchemy engine with mgf defaults.
|
|
42
|
+
|
|
43
|
+
``database_url`` follows the SQLAlchemy URL grammar — must use
|
|
44
|
+
an async driver:
|
|
45
|
+
|
|
46
|
+
- ``postgresql+asyncpg://user:pass@host/db``
|
|
47
|
+
- ``mysql+aiomysql://...``
|
|
48
|
+
- ``sqlite+aiosqlite://`` (in-memory) or
|
|
49
|
+
``sqlite+aiosqlite:///path/to/db.sqlite``
|
|
50
|
+
|
|
51
|
+
``echo=True`` logs every statement (debug only — never in prod).
|
|
52
|
+
|
|
53
|
+
Pool settings are passed through to SQLAlchemy. SQLite ignores
|
|
54
|
+
``pool_size`` / ``max_overflow`` (it uses ``StaticPool`` /
|
|
55
|
+
``NullPool``) — the values are still accepted for shape
|
|
56
|
+
consistency but have no effect.
|
|
57
|
+
|
|
58
|
+
``**extra`` is forwarded to :func:`create_async_engine` for
|
|
59
|
+
consumers needing rare options (``connect_args``, ``isolation_level``,
|
|
60
|
+
``poolclass``, etc.).
|
|
61
|
+
|
|
62
|
+
Returns the engine. The engine is **not yet connected** — the
|
|
63
|
+
first connection attempt happens lazily on the first
|
|
64
|
+
``await engine.connect()`` or ``async_sessionmaker()`` call.
|
|
65
|
+
"""
|
|
66
|
+
kwargs: dict[str, Any] = {
|
|
67
|
+
"echo": echo,
|
|
68
|
+
"pool_pre_ping": pool_pre_ping,
|
|
69
|
+
"pool_recycle": pool_recycle,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Pool sizing only applies to drivers that use a real pool.
|
|
73
|
+
# SQLite uses StaticPool / NullPool; passing pool_size to those
|
|
74
|
+
# raises. Detect the driver and skip pool kwargs accordingly.
|
|
75
|
+
is_sqlite = database_url.startswith(("sqlite://", "sqlite+"))
|
|
76
|
+
if not is_sqlite:
|
|
77
|
+
kwargs["pool_size"] = pool_size
|
|
78
|
+
kwargs["max_overflow"] = max_overflow
|
|
79
|
+
|
|
80
|
+
kwargs.update(extra)
|
|
81
|
+
return create_async_engine(database_url, **kwargs)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["create_engine"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Async sessionmaker + tenant-scoping helper.
|
|
2
|
+
|
|
3
|
+
Two helpers covering the request-handling lifecycle:
|
|
4
|
+
|
|
5
|
+
- :func:`create_sessionmaker` — wraps :class:`async_sessionmaker`
|
|
6
|
+
with mgf defaults (``expire_on_commit=False`` — SQLAlchemy 2's
|
|
7
|
+
recommended default for async; consumers can pass
|
|
8
|
+
``expire_on_commit=True`` to opt into the legacy behavior).
|
|
9
|
+
- :func:`tenant_session` — context manager that runs
|
|
10
|
+
``SET LOCAL app.current_tenant = '<uuid>'`` for Postgres RLS-based
|
|
11
|
+
multi-tenancy. The ``LOCAL`` qualifier scopes to the current
|
|
12
|
+
transaction; auto-resets on commit / rollback. Tenant id is
|
|
13
|
+
UUID-validated at the call site to prevent SQL injection.
|
|
14
|
+
|
|
15
|
+
The FastAPI-Depends-shaped ``get_session`` previously co-located here
|
|
16
|
+
was moved to :mod:`mgf.fastapi.db` in mgf-fastapi v0.2 — it depends
|
|
17
|
+
on Starlette/FastAPI's ``Request`` / ``app.state`` which would pull a
|
|
18
|
+
heavy framework dep into this otherwise-pure sibling.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from contextlib import asynccontextmanager
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
from uuid import UUID
|
|
26
|
+
|
|
27
|
+
from sqlalchemy import text
|
|
28
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import AsyncIterator
|
|
32
|
+
|
|
33
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_sessionmaker(
|
|
37
|
+
engine: AsyncEngine,
|
|
38
|
+
*,
|
|
39
|
+
expire_on_commit: bool = False,
|
|
40
|
+
) -> async_sessionmaker[AsyncSession]:
|
|
41
|
+
"""Return an :class:`async_sessionmaker` bound to ``engine``.
|
|
42
|
+
|
|
43
|
+
Default ``expire_on_commit=False`` matches SQLAlchemy 2's
|
|
44
|
+
recommended default for async sessions (objects stay usable
|
|
45
|
+
after commit instead of being silently expired). Pass
|
|
46
|
+
``expire_on_commit=True`` to revert to the legacy synchronous
|
|
47
|
+
semantics.
|
|
48
|
+
"""
|
|
49
|
+
return async_sessionmaker(
|
|
50
|
+
engine,
|
|
51
|
+
class_=AsyncSession,
|
|
52
|
+
expire_on_commit=expire_on_commit,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@asynccontextmanager
|
|
57
|
+
async def tenant_session(
|
|
58
|
+
session: AsyncSession,
|
|
59
|
+
tenant_id: str | UUID,
|
|
60
|
+
) -> AsyncIterator[AsyncSession]:
|
|
61
|
+
"""Run ``SET LOCAL app.current_tenant = '<tenant_id>'`` for ``session``.
|
|
62
|
+
|
|
63
|
+
Postgres RLS-based multi-tenancy pattern. The ``LOCAL`` qualifier
|
|
64
|
+
scopes the setting to the current transaction; auto-resets on
|
|
65
|
+
commit / rollback so cross-tenant leakage between requests is
|
|
66
|
+
impossible at the SQL layer.
|
|
67
|
+
|
|
68
|
+
``tenant_id`` is UUID-validated before interpolation. Postgres
|
|
69
|
+
``SET LOCAL`` does NOT accept bind parameters, so we have to
|
|
70
|
+
interpolate the value into the SQL — which is safe iff the
|
|
71
|
+
input is shape-validated (UUIDs are alphanumeric + dashes,
|
|
72
|
+
no SQL-meaningful chars). A non-UUID input raises ``ValueError``.
|
|
73
|
+
|
|
74
|
+
Raises ``ValueError`` if ``tenant_id`` isn't a UUID-shaped
|
|
75
|
+
string; raises whatever SQLAlchemy raises if the database
|
|
76
|
+
rejects the SET (e.g., custom GUC not declared in
|
|
77
|
+
``postgresql.conf``).
|
|
78
|
+
|
|
79
|
+
Use as::
|
|
80
|
+
|
|
81
|
+
async with sessionmaker() as session:
|
|
82
|
+
async with tenant_session(session, tenant_id) as scoped:
|
|
83
|
+
# All queries on `scoped` (same session, just
|
|
84
|
+
# tenant-scoped) get app.current_tenant.
|
|
85
|
+
rows = await scoped.execute(text("SELECT ..."))
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(tenant_id, UUID):
|
|
88
|
+
tenant_str = str(tenant_id)
|
|
89
|
+
else:
|
|
90
|
+
# Validate that the string is a real UUID; UUID() raises
|
|
91
|
+
# ValueError on malformed input. After validation, the
|
|
92
|
+
# canonical hex+dash form is safe to interpolate.
|
|
93
|
+
validated = UUID(tenant_id)
|
|
94
|
+
tenant_str = str(validated)
|
|
95
|
+
|
|
96
|
+
# SET LOCAL accepts only literal values; bind params don't work.
|
|
97
|
+
# Safe because tenant_str came from UUID() — alphanumeric+dashes.
|
|
98
|
+
await session.execute(text(f"SET LOCAL app.current_tenant = '{tenant_str}'"))
|
|
99
|
+
yield session
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"create_sessionmaker",
|
|
104
|
+
"tenant_session",
|
|
105
|
+
]
|
mgf/sqlalchemy/py.typed
ADDED
|
File without changes
|
|
@@ -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,8 @@
|
|
|
1
|
+
mgf/sqlalchemy/__init__.py,sha256=BG51-RLTY_RUCKRTtYiU7-3rSYHKE8m9V7J_UxTdfhg,2030
|
|
2
|
+
mgf/sqlalchemy/_engine.py,sha256=2ofgYljzOxjoeVp5kmv0Bhq2WDUsDFgaG2QkOTiPq8c,2891
|
|
3
|
+
mgf/sqlalchemy/_session.py,sha256=IRT6y61ODm9BuA3yx-mPWAjJACMR1CLkqjPcGbNq8vE,3850
|
|
4
|
+
mgf/sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
mgf_sqlalchemy-0.1.0.dist-info/METADATA,sha256=Me6qLm1JBn14u6lxmXBpYoInbZlvHqul5wUTMneqP8M,7580
|
|
6
|
+
mgf_sqlalchemy-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
mgf_sqlalchemy-0.1.0.dist-info/licenses/LICENSE,sha256=akPl7tlbK9cB--mcmU-3T3VYoIpSXQUJd3DDpztyDFc,1099
|
|
8
|
+
mgf_sqlalchemy-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|