FastSQLA 0.2.4__py3-none-any.whl → 0.3.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.
@@ -0,0 +1,359 @@
1
+ Metadata-Version: 2.2
2
+ Name: FastSQLA
3
+ Version: 0.3.0
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
+ Provides-Extra: docs
44
+ Requires-Dist: mkdocs-glightbox>=0.4.0; extra == "docs"
45
+ Requires-Dist: mkdocs-material>=9.5.50; extra == "docs"
46
+ Requires-Dist: mkdocstrings[python]>=0.27.0; extra == "docs"
47
+ Provides-Extra: sqlmodel
48
+ Requires-Dist: sqlmodel>=0.0.22; extra == "sqlmodel"
49
+
50
+ # FastSQLA
51
+
52
+ _Async SQLAlchemy 2.0+ for FastAPI — boilerplate, pagination, and seamless session management._
53
+
54
+ [![PyPI - Version](https://img.shields.io/pypi/v/FastSQLA?color=brightgreen)](https://pypi.org/project/FastSQLA/)
55
+ [![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)
56
+ [![Codecov](https://img.shields.io/codecov/c/github/hadrien/fastsqla?token=XK3YT60MWK&logo=codecov)](https://codecov.io/gh/hadrien/FastSQLA)
57
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg)](https://conventionalcommits.org)
58
+ [![GitHub License](https://img.shields.io/github/license/hadrien/fastsqla)](https://github.com/hadrien/FastSQLA/blob/main/LICENSE)
59
+ [![🍁 With love from Canada](https://img.shields.io/badge/With%20love%20from%20Canada-ffffff?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIiB2aWV3Qm94PSItMjAxNSAtMjAwMCA0MDMwIDQwMzAiPjxwYXRoIGZpbGw9IiNmMDAiIGQ9Im0tOTAgMjAzMCA0NS04NjNhOTUgOTUgMCAwIDAtMTExLTk4bC04NTkgMTUxIDExNi0zMjBhNjUgNjUgMCAwIDAtMjAtNzNsLTk0MS03NjIgMjEyLTk5YTY1IDY1IDAgMCAwIDM0LTc5bC0xODYtNTcyIDU0MiAxMTVhNjUgNjUgMCAwIDAgNzMtMzhsMTA1LTI0NyA0MjMgNDU0YTY1IDY1IDAgMCAwIDExMS01N2wtMjA0LTEwNTIgMzI3IDE4OWE2NSA2NSAwIDAgMCA5MS0yN2wzMzItNjUyIDMzMiA2NTJhNjUgNjUgMCAwIDAgOTEgMjdsMzI3LTE4OS0yMDQgMTA1MmE2NSA2NSAwIDAgMCAxMTEgNTdsNDIzLTQ1NCAxMDUgMjQ3YTY1IDY1IDAgMCAwIDczIDM4bDU0Mi0xMTUtMTg2IDU3MmE2NSA2NSAwIDAgMCAzNCA3OWwyMTIgOTktOTQxIDc2MmE2NSA2NSAwIDAgMC0yMCA3M2wxMTYgMzIwLTg1OS0xNTFhOTUgOTUgMCAwIDAtMTExIDk4bDQ1IDg2M3oiLz48L3N2Zz4K)](https://montrealpython.org)
60
+
61
+ **Documentation**: [https://hadrien.github.io/FastSQLA/](https://hadrien.github.io/FastSQLA/)
62
+
63
+ **Github Repo:** [https://github.com/hadrien/fastsqla](https://github.com/hadrien/fastsqla)
64
+
65
+ -----------------------------------------------------------------------------------------
66
+
67
+ `FastSQLA` is an async [`SQLAlchemy 2.0+`](https://docs.sqlalchemy.org/en/20/)
68
+ extension for [`FastAPI`](https://fastapi.tiangolo.com/) with built-in pagination,
69
+ [`SQLModel`](http://sqlmodel.tiangolo.com/) support and more.
70
+
71
+ It streamlines the configuration and asynchronous connection to relational databases by
72
+ providing boilerplate and intuitive helpers. Additionally, it offers built-in
73
+ customizable pagination and automatically manages the `SQLAlchemy` session lifecycle
74
+ 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).
75
+
76
+
77
+ ## Features
78
+
79
+ * Easy setup at app startup using
80
+ [`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan):
81
+
82
+ ```python
83
+ from fastapi import FastAPI
84
+ from fastsqla import lifespan
85
+
86
+ app = FastAPI(lifespan=lifespan)
87
+ ```
88
+
89
+ * `SQLAlchemy` async session dependency:
90
+
91
+ ```python
92
+ ...
93
+ from fastsqla import Session
94
+ from sqlalchemy import select
95
+ ...
96
+
97
+ @app.get("/heros")
98
+ async def get_heros(session:Session):
99
+ stmt = select(...)
100
+ result = await session.execute(stmt)
101
+ ...
102
+ ```
103
+
104
+ * `SQLAlchemy` async session with an async context manager:
105
+
106
+ ```python
107
+ from fastsqla import open_session
108
+
109
+ async def background_job():
110
+ async with open_session() as session:
111
+ stmt = select(...)
112
+ result = await session.execute(stmt)
113
+ ...
114
+ ```
115
+
116
+ * Built-in pagination:
117
+
118
+ ```python
119
+ ...
120
+ from fastsqla import Page, Paginate
121
+ from sqlalchemy import select
122
+ ...
123
+
124
+ @app.get("/heros", response_model=Page[HeroModel])
125
+ async def get_heros(paginate:Paginate):
126
+ return await paginate(select(Hero))
127
+ ```
128
+
129
+ <center>
130
+
131
+ 👇 `/heros?offset=10&limit=10` 👇
132
+
133
+ </center>
134
+
135
+ ```json
136
+ {
137
+ "data": [
138
+ {
139
+ "name": "The Flash",
140
+ "secret_identity": "Barry Allen",
141
+ "id": 11
142
+ },
143
+ {
144
+ "name": "Green Lantern",
145
+ "secret_identity": "Hal Jordan",
146
+ "id": 12
147
+ }
148
+ ],
149
+ "meta": {
150
+ "offset": 10,
151
+ "total_items": 12,
152
+ "total_pages": 2,
153
+ "page_number": 2
154
+ }
155
+ }
156
+ ```
157
+
158
+ * Pagination customization:
159
+ ```python
160
+ ...
161
+ from fastapi import Page, new_pagination
162
+ ...
163
+
164
+ Paginate = new_pagination(min_page_size=5, max_page_size=500)
165
+
166
+ @app.get("/heros", response_model=Page[HeroModel])
167
+ async def get_heros(paginate:Paginate):
168
+ return paginate(select(Hero))
169
+ ```
170
+ * Session lifecycle management: session is commited on request success or rollback on
171
+ failure.
172
+
173
+ * [`SQLModel`](http://sqlmodel.tiangolo.com/) support:
174
+ ```python
175
+ ...
176
+ from fastsqla import Item, Page, Paginate, Session
177
+ from sqlmodel import Field, SQLModel
178
+ ...
179
+
180
+ class Hero(SQLModel, table=True):
181
+ id: int | None = Field(default=None, primary_key=True)
182
+ name: str
183
+ secret_identity: str
184
+ age: int
185
+
186
+
187
+ @app.get("/heroes", response_model=Page[Hero])
188
+ async def get_heroes(paginate: Paginate):
189
+ return await paginate(select(Hero))
190
+
191
+
192
+ @app.get("/heroes/{hero_id}", response_model=Item[Hero])
193
+ async def get_hero(session: Session, hero_id: int):
194
+ hero = await session.get(Hero, hero_id)
195
+ if hero is None:
196
+ raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
197
+ return {"data": hero}
198
+ ```
199
+
200
+ ## Installing
201
+
202
+ Using [uv](https://docs.astral.sh/uv/):
203
+ ```bash
204
+ uv add fastsqla
205
+ ```
206
+
207
+ Using [pip](https://pip.pypa.io/):
208
+ ```
209
+ pip install fastsqla
210
+ ```
211
+
212
+ ## Quick Example
213
+
214
+ ### `example.py`
215
+
216
+ Let's write some tiny app in `example.py`:
217
+
218
+ ```python
219
+ # example.py
220
+ from http import HTTPStatus
221
+
222
+ from fastapi import FastAPI, HTTPException
223
+ from fastsqla import Base, Item, Page, Paginate, Session, lifespan
224
+ from pydantic import BaseModel, ConfigDict
225
+ from sqlalchemy import select
226
+ from sqlalchemy.exc import IntegrityError
227
+ from sqlalchemy.orm import Mapped, mapped_column
228
+
229
+
230
+ app = FastAPI(lifespan=lifespan)
231
+
232
+
233
+ class Hero(Base):
234
+ __tablename__ = "hero"
235
+ id: Mapped[int] = mapped_column(primary_key=True)
236
+ name: Mapped[str] = mapped_column(unique=True)
237
+ secret_identity: Mapped[str]
238
+ age: Mapped[int]
239
+
240
+
241
+ class HeroBase(BaseModel):
242
+ name: str
243
+ secret_identity: str
244
+ age: int
245
+
246
+
247
+ class HeroModel(HeroBase):
248
+ model_config = ConfigDict(from_attributes=True)
249
+ id: int
250
+
251
+
252
+ @app.get("/heros", response_model=Page[HeroModel])
253
+ async def list_heros(paginate: Paginate):
254
+ stmt = select(Hero)
255
+ return await paginate(stmt)
256
+
257
+
258
+ @app.get("/heros/{hero_id}", response_model=Item[HeroModel])
259
+ async def get_hero(hero_id: int, session: Session):
260
+ hero = await session.get(Hero, hero_id)
261
+ if hero is None:
262
+ raise HTTPException(HTTPStatus.NOT_FOUND, "Hero not found")
263
+ return {"data": hero}
264
+
265
+
266
+ @app.post("/heros", response_model=Item[HeroModel])
267
+ async def create_hero(new_hero: HeroBase, session: Session):
268
+ hero = Hero(**new_hero.model_dump())
269
+ session.add(hero)
270
+ try:
271
+ await session.flush()
272
+ except IntegrityError:
273
+ raise HTTPException(HTTPStatus.CONFLICT, "Duplicate hero name")
274
+ return {"data": hero}
275
+ ```
276
+
277
+ ### Database
278
+
279
+ 💡 This example uses an `SQLite` database for simplicity: `FastSQLA` is compatible with
280
+ all asynchronous db drivers that `SQLAlchemy` is compatible with.
281
+
282
+ Let's create an `SQLite` database using `sqlite3` and insert 12 rows in the `hero` table:
283
+
284
+ ```bash
285
+ sqlite3 db.sqlite <<EOF
286
+ -- Create Table hero
287
+ CREATE TABLE hero (
288
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
289
+ name TEXT NOT NULL UNIQUE, -- Unique hero name (e.g., Superman)
290
+ secret_identity TEXT NOT NULL, -- Secret identity (e.g., Clark Kent)
291
+ age INTEGER NOT NULL -- Age of the hero (e.g., 30)
292
+ );
293
+
294
+ -- Insert heroes with their name, secret identity, and age
295
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Superman', 'Clark Kent', 30);
296
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Batman', 'Bruce Wayne', 35);
297
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Wonder Woman', 'Diana Prince', 30);
298
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Iron Man', 'Tony Stark', 45);
299
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Spider-Man', 'Peter Parker', 25);
300
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Captain America', 'Steve Rogers', 100);
301
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Black Widow', 'Natasha Romanoff', 35);
302
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Thor', 'Thor Odinson', 1500);
303
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Scarlet Witch', 'Wanda Maximoff', 30);
304
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Doctor Strange', 'Stephen Strange', 40);
305
+ INSERT INTO hero (name, secret_identity, age) VALUES ('The Flash', 'Barry Allen', 28);
306
+ INSERT INTO hero (name, secret_identity, age) VALUES ('Green Lantern', 'Hal Jordan', 35);
307
+ EOF
308
+ ```
309
+
310
+ ### Run the app
311
+
312
+ Let's install required dependencies:
313
+ ```bash
314
+ pip install uvicorn aiosqlite fastsqla
315
+ ```
316
+ Let's run the app:
317
+ ```
318
+ sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false \
319
+ uvicorn example:app
320
+ ```
321
+
322
+ ### Check the result
323
+
324
+ Execute `GET /heros?offset=10&limit=10` using `curl`:
325
+ ```bash
326
+ curl -X 'GET' -H 'accept: application/json' 'http://127.0.0.1:8000/heros?offset=10&limit=10'
327
+ ```
328
+ Returns:
329
+ ```json
330
+ {
331
+ "data": [
332
+ {
333
+ "name": "The Flash",
334
+ "secret_identity": "Barry Allen",
335
+ "id": 11
336
+ },
337
+ {
338
+ "name": "Green Lantern",
339
+ "secret_identity": "Hal Jordan",
340
+ "id": 12
341
+ }
342
+ ],
343
+ "meta": {
344
+ "offset": 10,
345
+ "total_items": 12,
346
+ "total_pages": 2,
347
+ "page_number": 2
348
+ }
349
+ }
350
+ ```
351
+
352
+ You can also check the generated openapi doc by opening your browser to
353
+ [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).
354
+
355
+ ![OpenAPI generated documentation of the example API](https://raw.githubusercontent.com/hadrien/FastSQLA/refs/heads/main/docs/images/example-openapi-generated-doc.png)
356
+
357
+ ## License
358
+
359
+ This project is licensed under the terms of the [MIT license](https://github.com/hadrien/FastSQLA/blob/main/LICENSE).
@@ -0,0 +1,6 @@
1
+ fastsqla.py,sha256=yxfc96izBhaT8zobtm74OEigO5rfeCfmbRGZ5-jN3EM,11131
2
+ FastSQLA-0.3.0.dist-info/LICENSE,sha256=uNKcyfhTq0YUZxgSDiDGBHoJfflKjGWecSfWxpYe_O4,1070
3
+ FastSQLA-0.3.0.dist-info/METADATA,sha256=JuDDHA6vy8mzszl2C7iIqcPZvl8vLmQdNIPIKEMa5EM,12473
4
+ FastSQLA-0.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ FastSQLA-0.3.0.dist-info/top_level.txt,sha256=Uh-1ssTtuSS4_SYCBeDoDVOxqWTrRAPEBZkuih5isSE,9
6
+ FastSQLA-0.3.0.dist-info/RECORD,,
fastsqla.py CHANGED
@@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
4
4
  from contextlib import 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
 
@@ -44,7 +79,57 @@ class State(TypedDict):
44
79
 
45
80
 
46
81
  @asynccontextmanager
47
- async def lifespan(_) -> AsyncGenerator[State, None]:
82
+ async def lifespan(app: FastAPI) -> AsyncGenerator[State, None]:
83
+ """Use `fastsqla.lifespan` to set up SQLAlchemy.
84
+
85
+ In an ASGI application, [lifespan events](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
86
+ are used to communicate startup & shutdown events.
87
+
88
+ The [`lifespan`](https://fastapi.tiangolo.com/advanced/events/#lifespan) parameter of
89
+ the `FastAPI` app can be assigned to a context manager, which is opened when the app
90
+ starts and closed when the app stops.
91
+
92
+ In order for `FastSQLA` to setup `SQLAlchemy` before the app is started, set
93
+ `lifespan` parameter to `fastsqla.lifespan`:
94
+
95
+ ```python
96
+ from fastapi import FastAPI
97
+ from fastsqla import lifespan
98
+
99
+
100
+ app = FastAPI(lifespan=lifespan)
101
+ ```
102
+
103
+ If multiple lifespan contexts are required, create an async context manager function
104
+ to handle them and set it as the app's lifespan:
105
+
106
+ ```python
107
+ from collections.abc import AsyncGenerator
108
+ from contextlib import asynccontextmanager
109
+
110
+ from fastapi import FastAPI
111
+ from fastsqla import lifespan as fastsqla_lifespan
112
+ from this_other_library import another_lifespan
113
+
114
+
115
+ @asynccontextmanager
116
+ async def lifespan(app:FastAPI) -> AsyncGenerator[dict, None]:
117
+ async with AsyncExitStack() as stack:
118
+ yield {
119
+ **stack.enter_async_context(lifespan(app)),
120
+ **stack.enter_async_context(another_lifespan(app)),
121
+ }
122
+
123
+
124
+ app = FastAPI(lifespan=lifespan)
125
+ ```
126
+
127
+ To learn more about lifespan protocol:
128
+
129
+ * [Lifespan Protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
130
+ * [Use Lifespan State instead of `app.state`](https://github.com/Kludex/fastapi-tips?tab=readme-ov-file#6-use-lifespan-state-instead-of-appstate)
131
+ * [FastAPI lifespan documentation](https://fastapi.tiangolo.com/advanced/events/)
132
+ """
48
133
  prefix = "sqlalchemy_"
49
134
  sqla_config = {k.lower(): v for k, v in os.environ.items()}
50
135
  try:
@@ -70,6 +155,35 @@ async def lifespan(_) -> AsyncGenerator[State, None]:
70
155
 
71
156
  @asynccontextmanager
72
157
  async def open_session() -> AsyncGenerator[AsyncSession, None]:
158
+ """Async context manager that opens a new `SQLAlchemy` or `SQLModel` async session.
159
+
160
+ To the contrary of the [`Session`][fastsqla.Session] dependency which can only be
161
+ used in endpoints, `open_session` can be used anywhere such as in background tasks.
162
+
163
+ On exit, it automatically commits the session if no errors occur inside the context,
164
+ or rolls back when an exception is raised.
165
+ In all cases, it closes the session and returns the associated connection to the
166
+ connection pool.
167
+
168
+
169
+ Returns:
170
+ When `SQLModel` is not installed, an async generator that yields an
171
+ [`SQLAlchemy AsyncSession`][sqlalchemy.ext.asyncio.AsyncSession].
172
+
173
+ When `SQLModel` is installed, an async generator that yields an
174
+ [`SQLModel AsyncSession`](https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/ext/asyncio/session.py#L32)
175
+ which inherits from [`SQLAlchemy AsyncSession`][sqlalchemy.ext.asyncio.AsyncSession].
176
+
177
+
178
+ ```python
179
+ from fastsqla import open_session
180
+
181
+ async def example():
182
+ async with open_session() as session:
183
+ await session.execute(...)
184
+ ```
185
+
186
+ """
73
187
  session = SessionFactory()
74
188
  try:
75
189
  yield session
@@ -100,6 +214,54 @@ async def new_session() -> AsyncGenerator[AsyncSession, None]:
100
214
 
101
215
 
102
216
  Session = Annotated[AsyncSession, Depends(new_session)]
217
+ """Dependency used exclusively in endpoints to get an `SQLAlchemy` or `SQLModel` session.
218
+
219
+ `Session` is a [`FastAPI` dependency](https://fastapi.tiangolo.com/tutorial/dependencies/)
220
+ that provides an asynchronous `SQLAlchemy` session or `SQLModel` one if it's installed.
221
+ By defining an argument with type `Session` in an endpoint, `FastAPI` will automatically
222
+ inject an async session into the endpoint.
223
+
224
+ At the end of request handling:
225
+
226
+ * If no exceptions are raised, the session is automatically committed.
227
+ * If an exception is raised, the session is automatically rolled back.
228
+ * In alls cases, the session is closed and the associated connection is returned to the
229
+ connection pool.
230
+
231
+ Example:
232
+
233
+ ``` py title="example.py" hl_lines="3"
234
+ @app.get("/heros/{hero_id}", response_model=Item[HeroItem])
235
+ async def get_items(
236
+ session: Session, # (1)!
237
+ item_id: int,
238
+ ):
239
+ hero = await session.get(Hero, hero_id)
240
+ return {"data": hero}
241
+ ```
242
+
243
+ 1. Just define an argument with type `Session` to get an async session injected
244
+ in your endpoint.
245
+
246
+ ---
247
+
248
+ **Recommendation**: Unless there is a good reason to do so, avoid committing the session
249
+ manually, as `FastSQLA` handles it automatically.
250
+
251
+ If you need data generated by the database server, such as auto-incremented IDs, flush
252
+ the session instead:
253
+
254
+ ```python
255
+ @app.post("/heros", response_model=Item[HeroItem])
256
+ async def create_item(session: Session, new_hero: HeroBase):
257
+ hero = Hero(**new_hero.model_dump())
258
+ session.add(hero)
259
+ await session.flush()
260
+ return {"data": hero}
261
+ ```
262
+
263
+ Or use the [session context manager][fastsqla.open_session] instead.
264
+ """
103
265
 
104
266
 
105
267
  class Meta(BaseModel):
@@ -193,4 +355,23 @@ def new_pagination(
193
355
 
194
356
 
195
357
  type PaginateType[T] = Callable[[Select], Awaitable[Page[T]]]
358
+
196
359
  Paginate = Annotated[PaginateType[T], Depends(new_pagination())]
360
+ """A dependency used in endpoints to paginate `SQLAlchemy` select queries.
361
+
362
+ It adds **`offset`** and **`limit`** query parameters to the endpoint, which are used to
363
+ paginate. The model returned by the endpoint is a `Page` model. It contains a page of
364
+ data and metadata:
365
+
366
+ ```json
367
+ {
368
+ "data": List[T],
369
+ "meta": {
370
+ "offset": int,
371
+ "total_items": int,
372
+ "total_pages": int,
373
+ "page_number": int,
374
+ }
375
+ }
376
+ ```
377
+ """
@@ -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,,