FastSQLA 0.2.4__py3-none-any.whl → 0.4.4__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.
@@ -0,0 +1,357 @@
1
+ Metadata-Version: 2.4
2
+ Name: FastSQLA
3
+ Version: 0.4.4
4
+ Summary: SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination.
5
+ Author-email: Hadrien David <h@driendavid.com>
6
+ Project-URL: Homepage, https://github.com/hadrien/fastsqla
7
+ Project-URL: Documentation, https://github.com/hadrien/fastsqla
8
+ Project-URL: Repository, https://github.com/hadrien/fastsqla
9
+ Project-URL: Issues, https://github.com/hadrien/fastsqla/issues
10
+ Project-URL: Changelog, https://github.com/hadrien/fastsqla/releases
11
+ Keywords: FastAPI,SQLAlchemy,AsyncIO
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Framework :: Pydantic :: 2
17
+ Classifier: Framework :: Pydantic
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: Information Technology
20
+ Classifier: Intended Audience :: System Administrators
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python
26
+ Classifier: Programming Language :: SQL
27
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
28
+ Classifier: Topic :: Internet :: WWW/HTTP
29
+ Classifier: Topic :: Internet
30
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
31
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
32
+ Classifier: Topic :: Software Development :: Libraries
33
+ Classifier: Topic :: Software Development
34
+ Classifier: Typing :: Typed
35
+ Requires-Python: >=3.12
36
+ Description-Content-Type: text/markdown
37
+ Requires-Dist: fastapi>=0.115.6
38
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.37
39
+ Requires-Dist: structlog>=24.4.0
40
+ Provides-Extra: docs
41
+ Requires-Dist: mkdocs-glightbox>=0.4.0; extra == "docs"
42
+ Requires-Dist: mkdocs-llmstxt>=0.2.0; extra == "docs"
43
+ Requires-Dist: mkdocs-material>=9.5.50; extra == "docs"
44
+ Requires-Dist: mkdocstrings[python]>=0.27.0; extra == "docs"
45
+ Provides-Extra: sqlmodel
46
+ Requires-Dist: sqlmodel>=0.0.22; extra == "sqlmodel"
47
+
48
+ # FastSQLA
49
+
50
+ _Async SQLAlchemy 2.0+ for FastAPI — boilerplate, pagination, and seamless session management._
51
+
52
+ [![PyPI - Version](https://img.shields.io/pypi/v/FastSQLA?color=brightgreen)](https://pypi.org/project/FastSQLA/)
53
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/hadrien/fastsqla/ci.yml?branch=main&logo=github&label=CI)](https://github.com/hadrien/FastSQLA/actions?query=branch%3Amain+event%3Apush)
54
+ [![Codecov](https://img.shields.io/codecov/c/github/hadrien/fastsqla?token=XK3YT60MWK&logo=codecov)](https://codecov.io/gh/hadrien/FastSQLA)
55
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg)](https://conventionalcommits.org)
56
+ [![GitHub License](https://img.shields.io/github/license/hadrien/fastsqla)](https://github.com/hadrien/FastSQLA/blob/main/LICENSE)
57
+ [![🍁 With love from Canada](https://img.shields.io/badge/With%20love%20from%20Canada-ffffff?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIiB2aWV3Qm94PSItMjAxNSAtMjAwMCA0MDMwIDQwMzAiPjxwYXRoIGZpbGw9IiNmMDAiIGQ9Im0tOTAgMjAzMCA0NS04NjNhOTUgOTUgMCAwIDAtMTExLTk4bC04NTkgMTUxIDExNi0zMjBhNjUgNjUgMCAwIDAtMjAtNzNsLTk0MS03NjIgMjEyLTk5YTY1IDY1IDAgMCAwIDM0LTc5bC0xODYtNTcyIDU0MiAxMTVhNjUgNjUgMCAwIDAgNzMtMzhsMTA1LTI0NyA0MjMgNDU0YTY1IDY1IDAgMCAwIDExMS01N2wtMjA0LTEwNTIgMzI3IDE4OWE2NSA2NSAwIDAgMCA5MS0yN2wzMzItNjUyIDMzMiA2NTJhNjUgNjUgMCAwIDAgOTEgMjdsMzI3LTE4OS0yMDQgMTA1MmE2NSA2NSAwIDAgMCAxMTEgNTdsNDIzLTQ1NCAxMDUgMjQ3YTY1IDY1IDAgMCAwIDczIDM4bDU0Mi0xMTUtMTg2IDU3MmE2NSA2NSAwIDAgMCAzNCA3OWwyMTIgOTktOTQxIDc2MmE2NSA2NSAwIDAgMC0yMCA3M2wxMTYgMzIwLTg1OS0xNTFhOTUgOTUgMCAwIDAtMTExIDk4bDQ1IDg2M3oiLz48L3N2Zz4K)](https://montrealpython.org)
58
+
59
+ **Documentation**: [https://hadrien.github.io/FastSQLA/](https://hadrien.github.io/FastSQLA/)
60
+
61
+ **Github Repo:** [https://github.com/hadrien/fastsqla](https://github.com/hadrien/fastsqla)
62
+
63
+ -----------------------------------------------------------------------------------------
64
+
65
+ `FastSQLA` is an async [`SQLAlchemy 2.0+`](https://docs.sqlalchemy.org/en/20/)
66
+ extension for [`FastAPI`](https://fastapi.tiangolo.com/) with built-in pagination,
67
+ [`SQLModel`](http://sqlmodel.tiangolo.com/) support and more.
68
+
69
+ It streamlines the configuration and asynchronous connection to relational databases by
70
+ providing boilerplate and intuitive helpers. Additionally, it offers built-in
71
+ customizable pagination and automatically manages the `SQLAlchemy` session lifecycle
72
+ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it).
73
+
74
+
75
+ ## Features
76
+
77
+ * Easy setup at app startup using
78
+ [`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan):
79
+
80
+ ```python
81
+ from fastapi import FastAPI
82
+ from fastsqla import lifespan
83
+
84
+ app = FastAPI(lifespan=lifespan)
85
+ ```
86
+
87
+ * `SQLAlchemy` async session dependency:
88
+
89
+ ```python
90
+ ...
91
+ from fastsqla import Session
92
+ from sqlalchemy import select
93
+ ...
94
+
95
+ @app.get("/heros")
96
+ async def get_heros(session:Session):
97
+ stmt = select(...)
98
+ result = await session.execute(stmt)
99
+ ...
100
+ ```
101
+
102
+ * `SQLAlchemy` async session with an async context manager:
103
+
104
+ ```python
105
+ from fastsqla import open_session
106
+
107
+ async def background_job():
108
+ async with open_session() as session:
109
+ stmt = select(...)
110
+ result = await session.execute(stmt)
111
+ ...
112
+ ```
113
+
114
+ * Built-in pagination:
115
+
116
+ ```python
117
+ ...
118
+ from fastsqla import Page, Paginate
119
+ from sqlalchemy import select
120
+ ...
121
+
122
+ @app.get("/heros", response_model=Page[HeroModel])
123
+ async def get_heros(paginate:Paginate):
124
+ return await paginate(select(Hero))
125
+ ```
126
+
127
+ <center>
128
+
129
+ 👇 `/heros?offset=10&limit=10` 👇
130
+
131
+ </center>
132
+
133
+ ```json
134
+ {
135
+ "data": [
136
+ {
137
+ "name": "The Flash",
138
+ "secret_identity": "Barry Allen",
139
+ "id": 11
140
+ },
141
+ {
142
+ "name": "Green Lantern",
143
+ "secret_identity": "Hal Jordan",
144
+ "id": 12
145
+ }
146
+ ],
147
+ "meta": {
148
+ "offset": 10,
149
+ "total_items": 12,
150
+ "total_pages": 2,
151
+ "page_number": 2
152
+ }
153
+ }
154
+ ```
155
+
156
+ * Pagination customization:
157
+ ```python
158
+ ...
159
+ from fastapi import Page, new_pagination
160
+ ...
161
+
162
+ Paginate = new_pagination(min_page_size=5, max_page_size=500)
163
+
164
+ @app.get("/heros", response_model=Page[HeroModel])
165
+ async def get_heros(paginate:Paginate):
166
+ return paginate(select(Hero))
167
+ ```
168
+ * Session lifecycle management: session is commited on request success or rollback on
169
+ failure.
170
+
171
+ * [`SQLModel`](http://sqlmodel.tiangolo.com/) support:
172
+ ```python
173
+ ...
174
+ from fastsqla import Item, Page, Paginate, Session
175
+ from sqlmodel import Field, SQLModel
176
+ ...
177
+
178
+ class Hero(SQLModel, table=True):
179
+ id: int | None = Field(default=None, primary_key=True)
180
+ name: str
181
+ secret_identity: str
182
+ age: int
183
+
184
+
185
+ @app.get("/heroes", response_model=Page[Hero])
186
+ async def get_heroes(paginate: Paginate):
187
+ return await paginate(select(Hero))
188
+
189
+
190
+ @app.get("/heroes/{hero_id}", response_model=Item[Hero])
191
+ async def get_hero(session: Session, hero_id: int):
192
+ hero = await session.get(Hero, hero_id)
193
+ if hero is None:
194
+ raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
195
+ return {"data": hero}
196
+ ```
197
+
198
+ ## Installing
199
+
200
+ Using [uv](https://docs.astral.sh/uv/):
201
+ ```bash
202
+ uv add fastsqla
203
+ ```
204
+
205
+ Using [pip](https://pip.pypa.io/):
206
+ ```
207
+ pip install fastsqla
208
+ ```
209
+
210
+ ## Quick Example
211
+
212
+ ### `example.py`
213
+
214
+ Let's write some tiny app in `example.py`:
215
+
216
+ ```python
217
+ # example.py
218
+ from http import HTTPStatus
219
+
220
+ from fastapi import FastAPI, HTTPException
221
+ from fastsqla import Base, Item, Page, Paginate, Session, lifespan
222
+ from pydantic import BaseModel, ConfigDict
223
+ from sqlalchemy import select
224
+ from sqlalchemy.exc import IntegrityError
225
+ from sqlalchemy.orm import Mapped, mapped_column
226
+
227
+
228
+ app = FastAPI(lifespan=lifespan)
229
+
230
+
231
+ class Hero(Base):
232
+ __tablename__ = "hero"
233
+ id: Mapped[int] = mapped_column(primary_key=True)
234
+ name: Mapped[str] = mapped_column(unique=True)
235
+ secret_identity: Mapped[str]
236
+ age: Mapped[int]
237
+
238
+
239
+ class HeroBase(BaseModel):
240
+ name: str
241
+ secret_identity: str
242
+ age: int
243
+
244
+
245
+ class HeroModel(HeroBase):
246
+ model_config = ConfigDict(from_attributes=True)
247
+ id: int
248
+
249
+
250
+ @app.get("/heros", response_model=Page[HeroModel])
251
+ async def list_heros(paginate: Paginate):
252
+ stmt = select(Hero)
253
+ return await paginate(stmt)
254
+
255
+
256
+ @app.get("/heros/{hero_id}", response_model=Item[HeroModel])
257
+ async def get_hero(hero_id: int, session: Session):
258
+ hero = await session.get(Hero, hero_id)
259
+ if hero is None:
260
+ raise HTTPException(HTTPStatus.NOT_FOUND, "Hero not found")
261
+ return {"data": hero}
262
+
263
+
264
+ @app.post("/heros", response_model=Item[HeroModel])
265
+ async def create_hero(new_hero: HeroBase, session: Session):
266
+ hero = Hero(**new_hero.model_dump())
267
+ session.add(hero)
268
+ try:
269
+ await session.flush()
270
+ except IntegrityError:
271
+ raise HTTPException(HTTPStatus.CONFLICT, "Duplicate hero name")
272
+ return {"data": hero}
273
+ ```
274
+
275
+ ### Database
276
+
277
+ 💡 This example uses an `SQLite` database for simplicity: `FastSQLA` is compatible with
278
+ all asynchronous db drivers that `SQLAlchemy` is compatible with.
279
+
280
+ Let's create an `SQLite` database using `sqlite3` and insert 12 rows in the `hero` table:
281
+
282
+ ```bash
283
+ sqlite3 db.sqlite <<EOF
284
+ -- Create Table hero
285
+ CREATE TABLE hero (
286
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
287
+ name TEXT NOT NULL UNIQUE, -- Unique hero name (e.g., Superman)
288
+ secret_identity TEXT NOT NULL, -- Secret identity (e.g., Clark Kent)
289
+ age INTEGER NOT NULL -- Age of the hero (e.g., 30)
290
+ );
291
+
292
+ -- Insert heroes with their name, secret identity, and age
293
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Superman', 'Clark Kent', 30);
294
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Batman', 'Bruce Wayne', 35);
295
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Wonder Woman', 'Diana Prince', 30);
296
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Iron Man', 'Tony Stark', 45);
297
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Spider-Man', 'Peter Parker', 25);
298
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Captain America', 'Steve Rogers', 100);
299
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Black Widow', 'Natasha Romanoff', 35);
300
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Thor', 'Thor Odinson', 1500);
301
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Scarlet Witch', 'Wanda Maximoff', 30);
302
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Doctor Strange', 'Stephen Strange', 40);
303
+ INSERT INTO hero (name, secret_identity, age) VALUES ('The Flash', 'Barry Allen', 28);
304
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Green Lantern', 'Hal Jordan', 35);
305
+ EOF
306
+ ```
307
+
308
+ ### Run the app
309
+
310
+ Let's install required dependencies:
311
+ ```bash
312
+ pip install uvicorn aiosqlite fastsqla
313
+ ```
314
+ Let's run the app:
315
+ ```
316
+ sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false \
317
+ uvicorn example:app
318
+ ```
319
+
320
+ ### Check the result
321
+
322
+ Execute `GET /heros?offset=10&limit=10` using `curl`:
323
+ ```bash
324
+ curl -X 'GET' -H 'accept: application/json' 'http://127.0.0.1:8000/heros?offset=10&limit=10'
325
+ ```
326
+ Returns:
327
+ ```json
328
+ {
329
+ "data": [
330
+ {
331
+ "name": "The Flash",
332
+ "secret_identity": "Barry Allen",
333
+ "id": 11
334
+ },
335
+ {
336
+ "name": "Green Lantern",
337
+ "secret_identity": "Hal Jordan",
338
+ "id": 12
339
+ }
340
+ ],
341
+ "meta": {
342
+ "offset": 10,
343
+ "total_items": 12,
344
+ "total_pages": 2,
345
+ "page_number": 2
346
+ }
347
+ }
348
+ ```
349
+
350
+ You can also check the generated openapi doc by opening your browser to
351
+ [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).
352
+
353
+ ![OpenAPI generated documentation of the example API](https://raw.githubusercontent.com/hadrien/FastSQLA/refs/heads/main/docs/images/example-openapi-generated-doc.png)
354
+
355
+ ## License
356
+
357
+ This project is licensed under the terms of the [MIT license](https://github.com/hadrien/FastSQLA/blob/main/LICENSE).
@@ -0,0 +1,5 @@
1
+ fastsqla.py,sha256=wO4KpgkA3jVOCtpbwORlaUuOu4YsEQoRslzg9wjDNBk,12364
2
+ fastsqla-0.4.4.dist-info/METADATA,sha256=lyIlPmzzZ99nG1uKAmrpi1lH7Zpclod8LUTmNj0OOuo,12425
3
+ fastsqla-0.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ fastsqla-0.4.4.dist-info/top_level.txt,sha256=Uh-1ssTtuSS4_SYCBeDoDVOxqWTrRAPEBZkuih5isSE,9
5
+ fastsqla-0.4.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
fastsqla.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import math
2
2
  import os
3
3
  from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
4
- from contextlib import asynccontextmanager
4
+ from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
5
5
  from typing import Annotated, Generic, TypeVar, TypedDict
6
6
 
7
- from fastapi import Depends, Query
7
+ from fastapi import Depends, FastAPI, Query
8
8
  from pydantic import BaseModel, Field
9
9
  from sqlalchemy import Result, Select, func, select
10
10
  from sqlalchemy.ext.asyncio import (
@@ -17,6 +17,15 @@ from sqlalchemy.ext.declarative import DeferredReflection
17
17
  from sqlalchemy.orm import DeclarativeBase
18
18
  from structlog import get_logger
19
19
 
20
+ logger = get_logger(__name__)
21
+
22
+ try:
23
+ from sqlmodel.ext.asyncio.session import AsyncSession
24
+
25
+ except ImportError:
26
+ pass
27
+
28
+
20
29
  __all__ = [
21
30
  "Base",
22
31
  "Collection",
@@ -30,12 +39,38 @@ __all__ = [
30
39
  "open_session",
31
40
  ]
32
41
 
33
- SessionFactory = async_sessionmaker(expire_on_commit=False)
42
+ SessionFactory = async_sessionmaker(expire_on_commit=False, class_=AsyncSession)
34
43
 
35
44
  logger = get_logger(__name__)
36
45
 
37
46
 
38
47
  class Base(DeclarativeBase, DeferredReflection):
48
+ """Inherit from `Base` to declare an `SQLAlchemy` model.
49
+
50
+ Example:
51
+ ```py
52
+ from fastsqla import Base
53
+ from sqlalchemy.orm import Mapped, mapped_column
54
+
55
+
56
+ class Hero(Base):
57
+ __tablename__ = "hero"
58
+ id: Mapped[int] = mapped_column(primary_key=True)
59
+ name: Mapped[str] = mapped_column(unique=True)
60
+ secret_identity: Mapped[str]
61
+ age: Mapped[int]
62
+ ```
63
+
64
+ To learn more on `SQLAlchemy` ORM & Declarative mapping:
65
+
66
+ * [ORM Quick Start](https://docs.sqlalchemy.org/en/20/orm/quickstart.html)
67
+ * [Declarative Mapping](https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#declarative-mapping)
68
+
69
+ !!! note
70
+
71
+ You don't need this if you use [`SQLModel`](http://sqlmodel.tiangolo.com/).
72
+ """
73
+
39
74
  __abstract__ = True
40
75
 
41
76
 
@@ -43,33 +78,151 @@ class State(TypedDict):
43
78
  fastsqla_engine: AsyncEngine
44
79
 
45
80
 
46
- @asynccontextmanager
47
- async def lifespan(_) -> AsyncGenerator[State, None]:
48
- prefix = "sqlalchemy_"
49
- sqla_config = {k.lower(): v for k, v in os.environ.items()}
50
- try:
51
- engine = async_engine_from_config(sqla_config, prefix=prefix)
81
+ def new_lifespan(
82
+ url: str | None = None, **kw
83
+ ) -> Callable[[FastAPI], _AsyncGeneratorContextManager[State, None]]:
84
+ """Create a new lifespan async context manager.
85
+
86
+ It expects the exact same parameters as
87
+ [`sqlalchemy.ext.asyncio.create_async_engine`][sqlalchemy.ext.asyncio.create_async_engine]
88
+
89
+ Example:
90
+
91
+ ```python
92
+ from fastapi import FastAPI
93
+ from fastsqla import new_lifespan
52
94
 
53
- except KeyError as exc:
54
- raise Exception(f"Missing {prefix}{exc.args[0]} in environ.") from exc
95
+ lifespan = new_lifespan(
96
+ "sqlite+aiosqlite:///app/db.sqlite", connect_args={"autocommit": False}
97
+ )
98
+
99
+ app = FastAPI(lifespan=lifespan)
100
+ ```
101
+
102
+ Args:
103
+ url (str): Database url.
104
+ kw (dict): Configuration parameters as expected by [`sqlalchemy.ext.asyncio.create_async_engine`][sqlalchemy.ext.asyncio.create_async_engine]
105
+ """
106
+
107
+ has_config = url is not None
108
+
109
+ @asynccontextmanager
110
+ async def lifespan(app: FastAPI) -> AsyncGenerator[State, None]:
111
+ if has_config:
112
+ prefix = ""
113
+ sqla_config = {**kw, **{"url": url}}
114
+
115
+ else:
116
+ prefix = "sqlalchemy_"
117
+ sqla_config = {k.lower(): v for k, v in os.environ.items()}
118
+
119
+ try:
120
+ engine = async_engine_from_config(sqla_config, prefix=prefix)
121
+
122
+ except KeyError as exc:
123
+ raise Exception(f"Missing {prefix}{exc.args[0]} in environ.") from exc
124
+
125
+ async with engine.begin() as conn:
126
+ await conn.run_sync(Base.prepare)
55
127
 
56
- async with engine.begin() as conn:
57
- await conn.run_sync(Base.prepare)
128
+ SessionFactory.configure(bind=engine)
58
129
 
59
- SessionFactory.configure(bind=engine)
130
+ await logger.ainfo("Configured SQLAlchemy.")
60
131
 
61
- await logger.ainfo("Configured SQLAlchemy.")
132
+ yield {"fastsqla_engine": engine}
62
133
 
63
- yield {"fastsqla_engine": engine}
134
+ SessionFactory.configure(bind=None)
135
+ await engine.dispose()
64
136
 
65
- SessionFactory.configure(bind=None)
66
- await engine.dispose()
137
+ await logger.ainfo("Cleared SQLAlchemy config.")
67
138
 
68
- await logger.ainfo("Cleared SQLAlchemy config.")
139
+ return lifespan
140
+
141
+
142
+ lifespan = new_lifespan()
143
+ """Use `fastsqla.lifespan` to set up SQLAlchemy directly from environment variables.
144
+
145
+ In an ASGI application, [lifespan events](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
146
+ are used to communicate startup & shutdown events.
147
+
148
+ The [`lifespan`](https://fastapi.tiangolo.com/advanced/events/#lifespan) parameter of
149
+ the `FastAPI` app can be assigned to a context manager, which is opened when the app
150
+ starts and closed when the app stops.
151
+
152
+ In order for `FastSQLA` to setup `SQLAlchemy` before the app is started, set
153
+ `lifespan` parameter to `fastsqla.lifespan`:
154
+
155
+ ```python
156
+ from fastapi import FastAPI
157
+ from fastsqla import lifespan
158
+
159
+
160
+ app = FastAPI(lifespan=lifespan)
161
+ ```
162
+
163
+ If multiple lifespan contexts are required, create an async context manager function
164
+ to handle them and set it as the app's lifespan:
165
+
166
+ ```python
167
+ from collections.abc import AsyncGenerator
168
+ from contextlib import asynccontextmanager
169
+
170
+ from fastapi import FastAPI
171
+ from fastsqla import lifespan as fastsqla_lifespan
172
+ from this_other_library import another_lifespan
173
+
174
+
175
+ @asynccontextmanager
176
+ async def lifespan(app:FastAPI) -> AsyncGenerator[dict, None]:
177
+ async with AsyncExitStack() as stack:
178
+ yield {
179
+ **stack.enter_async_context(lifespan(app)),
180
+ **stack.enter_async_context(another_lifespan(app)),
181
+ }
182
+
183
+
184
+ app = FastAPI(lifespan=lifespan)
185
+ ```
186
+
187
+ To learn more about lifespan protocol:
188
+
189
+ * [Lifespan Protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
190
+ * [Use Lifespan State instead of `app.state`](https://github.com/Kludex/fastapi-tips?tab=readme-ov-file#6-use-lifespan-state-instead-of-appstate)
191
+ * [FastAPI lifespan documentation](https://fastapi.tiangolo.com/advanced/events/)
192
+ """
69
193
 
70
194
 
71
195
  @asynccontextmanager
72
196
  async def open_session() -> AsyncGenerator[AsyncSession, None]:
197
+ """Async context manager that opens a new `SQLAlchemy` or `SQLModel` async session.
198
+
199
+ To the contrary of the [`Session`][fastsqla.Session] dependency which can only be
200
+ used in endpoints, `open_session` can be used anywhere such as in background tasks.
201
+
202
+ On exit, it automatically commits the session if no errors occur inside the context,
203
+ or rolls back when an exception is raised.
204
+ In all cases, it closes the session and returns the associated connection to the
205
+ connection pool.
206
+
207
+
208
+ Returns:
209
+ When `SQLModel` is not installed, an async generator that yields an
210
+ [`SQLAlchemy AsyncSession`][sqlalchemy.ext.asyncio.AsyncSession].
211
+
212
+ When `SQLModel` is installed, an async generator that yields an
213
+ [`SQLModel AsyncSession`](https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/ext/asyncio/session.py#L32)
214
+ which inherits from [`SQLAlchemy AsyncSession`][sqlalchemy.ext.asyncio.AsyncSession].
215
+
216
+
217
+ ```python
218
+ from fastsqla import open_session
219
+
220
+ async def example():
221
+ async with open_session() as session:
222
+ await session.execute(...)
223
+ ```
224
+
225
+ """
73
226
  session = SessionFactory()
74
227
  try:
75
228
  yield session
@@ -100,6 +253,61 @@ async def new_session() -> AsyncGenerator[AsyncSession, None]:
100
253
 
101
254
 
102
255
  Session = Annotated[AsyncSession, Depends(new_session)]
256
+ """Dependency used exclusively in endpoints to get an `SQLAlchemy` or `SQLModel` session.
257
+
258
+ `Session` is a [`FastAPI` dependency](https://fastapi.tiangolo.com/tutorial/dependencies/)
259
+ that provides an asynchronous `SQLAlchemy` session or `SQLModel` one if it's installed.
260
+ By defining an argument with type `Session` in an endpoint, `FastAPI` will automatically
261
+ inject an async session into the endpoint.
262
+
263
+ At the end of request handling:
264
+
265
+ * If no exceptions are raised, the session is automatically committed.
266
+ * If an exception is raised, the session is automatically rolled back.
267
+ * In all cases, the session is closed and the associated connection is returned to the
268
+ connection pool.
269
+
270
+ Example:
271
+
272
+ ``` py title="example.py" hl_lines="6"
273
+ from fastsqla import Item, Session
274
+ ...
275
+
276
+ @app.get("/heros/{hero_id}", response_model=Item[HeroItem])
277
+ async def get_items(
278
+ session: Session, # (1)!
279
+ item_id: int,
280
+ ):
281
+ hero = await session.get(Hero, hero_id)
282
+ return {"data": hero}
283
+ ```
284
+
285
+ 1. Just define an argument with type `Session` to get an async session injected
286
+ in your endpoint.
287
+
288
+ ---
289
+
290
+ **Recommendation**: Unless there is a good reason to do so, avoid committing the session
291
+ manually, as `FastSQLA` handles it automatically.
292
+
293
+ If you need data generated by the database server, such as auto-incremented IDs, flush
294
+ the session instead:
295
+
296
+ ```python
297
+ from fastsqla import Item, Session
298
+ ...
299
+
300
+
301
+ @app.post("/heros", response_model=Item[HeroItem])
302
+ async def create_item(session: Session, new_hero: HeroBase):
303
+ hero = Hero(**new_hero.model_dump())
304
+ session.add(hero)
305
+ await session.flush()
306
+ return {"data": hero}
307
+ ```
308
+
309
+ Or use the [session context manager][fastsqla.open_session] instead.
310
+ """
103
311
 
104
312
 
105
313
  class Meta(BaseModel):
@@ -121,6 +329,23 @@ class Collection(BaseModel, Generic[T]):
121
329
 
122
330
 
123
331
  class Page(Collection[T]):
332
+ """Generic container that contains collection data and page metadata.
333
+
334
+ The `Page` model is used to return paginated data in paginated endpoints:
335
+
336
+ ```json
337
+ {
338
+ "data": list[T],
339
+ "meta": {
340
+ "offset": int,
341
+ "total_items": int,
342
+ "total_pages": int,
343
+ "page_number": int,
344
+ }
345
+ }
346
+ ```
347
+ """
348
+
124
349
  meta: Meta
125
350
 
126
351
 
@@ -193,4 +418,10 @@ def new_pagination(
193
418
 
194
419
 
195
420
  type PaginateType[T] = Callable[[Select], Awaitable[Page[T]]]
421
+
196
422
  Paginate = Annotated[PaginateType[T], Depends(new_pagination())]
423
+ """A dependency used in endpoints to paginate `SQLAlchemy` select queries.
424
+
425
+ It adds **`offset`** and **`limit`** query parameters to the endpoint, which are used to
426
+ paginate. The model returned by the endpoint is a [`Page`][fastsqla.Page] model.
427
+ """
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Hadrien David
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.
@@ -1,261 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: FastSQLA
3
- Version: 0.2.4
4
- Summary: SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination.
5
- Author-email: Hadrien David <bonjour@hadriendavid.com>
6
- License: MIT License
7
- Project-URL: Homepage, https://github.com/hadrien/fastsqla
8
- Project-URL: Documentation, https://github.com/hadrien/fastsqla
9
- Project-URL: Repository, https://github.com/hadrien/fastsqla
10
- Project-URL: Issues, https://github.com/hadrien/fastsqla/issues
11
- Project-URL: Changelog, https://github.com/hadrien/fastsqla/releases
12
- Keywords: FastAPI,SQLAlchemy,AsyncIO
13
- Classifier: Development Status :: 4 - Beta
14
- Classifier: Environment :: Web Environment
15
- Classifier: Framework :: AsyncIO
16
- Classifier: Framework :: FastAPI
17
- Classifier: Framework :: Pydantic :: 2
18
- Classifier: Framework :: Pydantic
19
- Classifier: Intended Audience :: Developers
20
- Classifier: Intended Audience :: Information Technology
21
- Classifier: Intended Audience :: System Administrators
22
- Classifier: License :: OSI Approved :: MIT License
23
- Classifier: Operating System :: OS Independent
24
- Classifier: Programming Language :: Python :: 3 :: Only
25
- Classifier: Programming Language :: Python :: 3.12
26
- Classifier: Programming Language :: Python :: 3
27
- Classifier: Programming Language :: Python
28
- Classifier: Programming Language :: SQL
29
- Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
30
- Classifier: Topic :: Internet :: WWW/HTTP
31
- Classifier: Topic :: Internet
32
- Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
33
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
- Classifier: Topic :: Software Development :: Libraries
35
- Classifier: Topic :: Software Development
36
- Classifier: Typing :: Typed
37
- Requires-Python: >=3.12
38
- Description-Content-Type: text/markdown
39
- License-File: LICENSE
40
- Requires-Dist: fastapi>=0.115.6
41
- Requires-Dist: sqlalchemy[asyncio]>=2.0.37
42
- Requires-Dist: structlog>=24.4.0
43
-
44
- # 🚀 FastSQLA
45
-
46
- [![PyPI - Version](https://img.shields.io/pypi/v/FastSQLA?color=brightgreen)](https://pypi.org/project/FastSQLA/)
47
- [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg)](https://conventionalcommits.org)
48
- [![codecov](https://codecov.io/gh/hadrien/fastsqla/graph/badge.svg?token=XK3YT60MWK)](https://codecov.io/gh/hadrien/fastsqla)
49
-
50
- `FastSQLA` is an [`SQLAlchemy`] extension for [`FastAPI`].
51
- It supports asynchronous `SQLAlchemy` sessions and includes built-in custimizable
52
- pagination.
53
-
54
- ## Features
55
-
56
- <details>
57
- <summary>Automatic SQLAlchemy configuration at app startup.</summary>
58
-
59
- Using [`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan):
60
- ```python
61
- from fastapi import FastAPI
62
- from fastsqla import lifespan
63
-
64
- app = FastAPI(lifespan=lifespan)
65
- ```
66
- </details>
67
- <details>
68
- <summary>Async SQLAlchemy session as a FastAPI dependency.</summary>
69
-
70
- ```python
71
- ...
72
- from fastsqla import Session
73
- from sqlalchemy import select
74
- ...
75
-
76
- @app.get("/heros")
77
- async def get_heros(session:Session):
78
- stmt = select(...)
79
- result = await session.execute(stmt)
80
- ...
81
- ```
82
- </details>
83
- <details>
84
- <summary>Built-in pagination.</summary>
85
-
86
- ```python
87
- ...
88
- from fastsqla import Page, Paginate
89
- from sqlalchemy import select
90
- ...
91
-
92
- @app.get("/heros", response_model=Page[HeroModel])
93
- async def get_heros(paginate:Paginate):
94
- return paginate(select(Hero))
95
- ```
96
- </details>
97
- <details>
98
- <summary>Allows pagination customization.</summary>
99
-
100
- ```python
101
- ...
102
- from fastapi import new_pagination
103
- ...
104
-
105
- Paginate = new_pagination(min_page_size=5, max_page_size=500)
106
-
107
- @app.get("/heros", response_model=Page[HeroModel])
108
- async def get_heros(paginate:Paginate):
109
- return paginate(select(Hero))
110
- ```
111
- </details>
112
-
113
- And more ...
114
- <!-- <details><summary></summary></details> -->
115
-
116
- ## Installing
117
-
118
- Using [uv](https://docs.astral.sh/uv/):
119
- ```bash
120
- uv add fastsqla
121
- ```
122
-
123
- Using [pip](https://pip.pypa.io/):
124
- ```
125
- pip install fastsqla
126
- ```
127
-
128
- ## Quick Example
129
-
130
- ```python
131
- # example.py
132
- from http import HTTPStatus
133
-
134
- from fastapi import FastAPI, HTTPException
135
- from pydantic import BaseModel, ConfigDict
136
- from sqlalchemy import select
137
- from sqlalchemy.exc import IntegrityError
138
- from sqlalchemy.orm import Mapped, mapped_column
139
-
140
- from fastsqla import Base, Item, Page, Paginate, Session, lifespan
141
-
142
- app = FastAPI(lifespan=lifespan)
143
-
144
-
145
- class Hero(Base):
146
- __tablename__ = "hero"
147
- id: Mapped[int] = mapped_column(primary_key=True)
148
- name: Mapped[str] = mapped_column(unique=True)
149
- secret_identity: Mapped[str]
150
-
151
-
152
- class HeroBase(BaseModel):
153
- name: str
154
- secret_identity: str
155
-
156
-
157
- class HeroModel(HeroBase):
158
- model_config = ConfigDict(from_attributes=True)
159
- id: int
160
-
161
-
162
- @app.get("/heros", response_model=Page[HeroModel])
163
- async def list_users(paginate: Paginate):
164
- return await paginate(select(Hero))
165
-
166
-
167
- @app.get("/heros/{hero_id}", response_model=Item[HeroModel])
168
- async def get_user(hero_id: int, session: Session):
169
- hero = await session.get(Hero, hero_id)
170
- if hero is None:
171
- raise HTTPException(HTTPStatus.NOT_FOUND, "Hero not found")
172
- return {"data": hero}
173
-
174
-
175
- @app.post("/heros", response_model=Item[HeroModel])
176
- async def create_user(new_hero: HeroBase, session: Session):
177
- hero = Hero(**new_hero.model_dump())
178
- session.add(hero)
179
- try:
180
- await session.flush()
181
- except IntegrityError:
182
- raise HTTPException(HTTPStatus.CONFLICT, "Duplicate hero name")
183
- return {"data": hero}
184
- ```
185
-
186
- > [!NOTE]
187
- > Sqlite is used for the sake of the example.
188
- > FastSQLA is compatible with all async db drivers that SQLAlchemy is compatible with.
189
-
190
- <details>
191
- <summary>Create an <code>sqlite3</code> db:</summary>
192
-
193
- ```bash
194
- sqlite3 db.sqlite <<EOF
195
- CREATE TABLE hero (
196
- id INTEGER PRIMARY KEY AUTOINCREMENT,
197
- name TEXT NOT NULL UNIQUE, -- Hero name (e.g., Superman)
198
- secret_identity TEXT NOT NULL -- Secret identity (e.g., Clark Kent)
199
- );
200
-
201
- -- Insert heroes with hero name and secret identity
202
- INSERT INTO hero (name, secret_identity) VALUES ('Superman', 'Clark Kent');
203
- INSERT INTO hero (name, secret_identity) VALUES ('Batman', 'Bruce Wayne');
204
- INSERT INTO hero (name, secret_identity) VALUES ('Wonder Woman', 'Diana Prince');
205
- INSERT INTO hero (name, secret_identity) VALUES ('Iron Man', 'Tony Stark');
206
- INSERT INTO hero (name, secret_identity) VALUES ('Spider-Man', 'Peter Parker');
207
- INSERT INTO hero (name, secret_identity) VALUES ('Captain America', 'Steve Rogers');
208
- INSERT INTO hero (name, secret_identity) VALUES ('Black Widow', 'Natasha Romanoff');
209
- INSERT INTO hero (name, secret_identity) VALUES ('Thor', 'Thor Odinson');
210
- INSERT INTO hero (name, secret_identity) VALUES ('Scarlet Witch', 'Wanda Maximoff');
211
- INSERT INTO hero (name, secret_identity) VALUES ('Doctor Strange', 'Stephen Strange');
212
- INSERT INTO hero (name, secret_identity) VALUES ('The Flash', 'Barry Allen');
213
- INSERT INTO hero (name, secret_identity) VALUES ('Green Lantern', 'Hal Jordan');
214
- EOF
215
- ```
216
-
217
- </details>
218
-
219
- <details>
220
- <summary>Install dependencies & run the app</summary>
221
-
222
- ```bash
223
- pip install uvicorn aiosqlite fastsqla
224
- sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false uvicorn example:app
225
- ```
226
-
227
- </details>
228
-
229
- Execute `GET /heros?offset=10`:
230
-
231
- ```bash
232
- curl -X 'GET' \
233
- 'http://127.0.0.1:8000/heros?offset=10&limit=10' \
234
- -H 'accept: application/json'
235
- ```
236
- Returns:
237
- ```json
238
- {
239
- "data": [
240
- {
241
- "name": "The Flash",
242
- "secret_identity": "Barry Allen",
243
- "id": 11
244
- },
245
- {
246
- "name": "Green Lantern",
247
- "secret_identity": "Hal Jordan",
248
- "id": 12
249
- }
250
- ],
251
- "meta": {
252
- "offset": 10,
253
- "total_items": 12,
254
- "total_pages": 2,
255
- "page_number": 2
256
- }
257
- }
258
- ```
259
-
260
- [`FastAPI`]: https://fastapi.tiangolo.com/
261
- [`SQLAlchemy`]: http://sqlalchemy.org/
@@ -1,6 +0,0 @@
1
- fastsqla.py,sha256=FRN1RhVQPlD7hMNZG7wvSWZ7qdfXaqjmB3pXVa3z6Yw,5188
2
- FastSQLA-0.2.4.dist-info/LICENSE,sha256=uNKcyfhTq0YUZxgSDiDGBHoJfflKjGWecSfWxpYe_O4,1070
3
- FastSQLA-0.2.4.dist-info/METADATA,sha256=b0Lb3jCdUsQEEOLdW881z5nlLtfXNwaH3ir8PQRcSuw,7658
4
- FastSQLA-0.2.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
- FastSQLA-0.2.4.dist-info/top_level.txt,sha256=Uh-1ssTtuSS4_SYCBeDoDVOxqWTrRAPEBZkuih5isSE,9
6
- FastSQLA-0.2.4.dist-info/RECORD,,