FastSQLA 0.2.4__tar.gz → 0.3.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.
- fastsqla-0.3.0/PKG-INFO +359 -0
- fastsqla-0.3.0/README.md +310 -0
- {fastsqla-0.2.4 → fastsqla-0.3.0}/pyproject.toml +30 -2
- fastsqla-0.3.0/src/FastSQLA.egg-info/PKG-INFO +359 -0
- fastsqla-0.3.0/src/FastSQLA.egg-info/requires.txt +11 -0
- fastsqla-0.3.0/src/fastsqla.py +377 -0
- fastsqla-0.2.4/PKG-INFO +0 -261
- fastsqla-0.2.4/README.md +0 -218
- fastsqla-0.2.4/src/FastSQLA.egg-info/PKG-INFO +0 -261
- fastsqla-0.2.4/src/FastSQLA.egg-info/requires.txt +0 -3
- fastsqla-0.2.4/src/fastsqla.py +0 -196
- {fastsqla-0.2.4 → fastsqla-0.3.0}/LICENSE +0 -0
- {fastsqla-0.2.4 → fastsqla-0.3.0}/setup.cfg +0 -0
- {fastsqla-0.2.4 → fastsqla-0.3.0}/src/FastSQLA.egg-info/SOURCES.txt +0 -0
- {fastsqla-0.2.4 → fastsqla-0.3.0}/src/FastSQLA.egg-info/dependency_links.txt +0 -0
- {fastsqla-0.2.4 → fastsqla-0.3.0}/src/FastSQLA.egg-info/top_level.txt +0 -0
fastsqla-0.3.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/FastSQLA/)
|
|
55
|
+
[](https://github.com/hadrien/FastSQLA/actions?query=branch%3Amain+event%3Apush)
|
|
56
|
+
[](https://codecov.io/gh/hadrien/FastSQLA)
|
|
57
|
+
[](https://conventionalcommits.org)
|
|
58
|
+
[](https://github.com/hadrien/FastSQLA/blob/main/LICENSE)
|
|
59
|
+
[](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
|
+

|
|
356
|
+
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
This project is licensed under the terms of the [MIT license](https://github.com/hadrien/FastSQLA/blob/main/LICENSE).
|
fastsqla-0.3.0/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# FastSQLA
|
|
2
|
+
|
|
3
|
+
_Async SQLAlchemy 2.0+ for FastAPI — boilerplate, pagination, and seamless session management._
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/FastSQLA/)
|
|
6
|
+
[](https://github.com/hadrien/FastSQLA/actions?query=branch%3Amain+event%3Apush)
|
|
7
|
+
[](https://codecov.io/gh/hadrien/FastSQLA)
|
|
8
|
+
[](https://conventionalcommits.org)
|
|
9
|
+
[](https://github.com/hadrien/FastSQLA/blob/main/LICENSE)
|
|
10
|
+
[](https://montrealpython.org)
|
|
11
|
+
|
|
12
|
+
**Documentation**: [https://hadrien.github.io/FastSQLA/](https://hadrien.github.io/FastSQLA/)
|
|
13
|
+
|
|
14
|
+
**Github Repo:** [https://github.com/hadrien/fastsqla](https://github.com/hadrien/fastsqla)
|
|
15
|
+
|
|
16
|
+
-----------------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
`FastSQLA` is an async [`SQLAlchemy 2.0+`](https://docs.sqlalchemy.org/en/20/)
|
|
19
|
+
extension for [`FastAPI`](https://fastapi.tiangolo.com/) with built-in pagination,
|
|
20
|
+
[`SQLModel`](http://sqlmodel.tiangolo.com/) support and more.
|
|
21
|
+
|
|
22
|
+
It streamlines the configuration and asynchronous connection to relational databases by
|
|
23
|
+
providing boilerplate and intuitive helpers. Additionally, it offers built-in
|
|
24
|
+
customizable pagination and automatically manages the `SQLAlchemy` session lifecycle
|
|
25
|
+
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).
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* Easy setup at app startup using
|
|
31
|
+
[`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan):
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from fastapi import FastAPI
|
|
35
|
+
from fastsqla import lifespan
|
|
36
|
+
|
|
37
|
+
app = FastAPI(lifespan=lifespan)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
* `SQLAlchemy` async session dependency:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
...
|
|
44
|
+
from fastsqla import Session
|
|
45
|
+
from sqlalchemy import select
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
@app.get("/heros")
|
|
49
|
+
async def get_heros(session:Session):
|
|
50
|
+
stmt = select(...)
|
|
51
|
+
result = await session.execute(stmt)
|
|
52
|
+
...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
* `SQLAlchemy` async session with an async context manager:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastsqla import open_session
|
|
59
|
+
|
|
60
|
+
async def background_job():
|
|
61
|
+
async with open_session() as session:
|
|
62
|
+
stmt = select(...)
|
|
63
|
+
result = await session.execute(stmt)
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
* Built-in pagination:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
...
|
|
71
|
+
from fastsqla import Page, Paginate
|
|
72
|
+
from sqlalchemy import select
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@app.get("/heros", response_model=Page[HeroModel])
|
|
76
|
+
async def get_heros(paginate:Paginate):
|
|
77
|
+
return await paginate(select(Hero))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
<center>
|
|
81
|
+
|
|
82
|
+
👇 `/heros?offset=10&limit=10` 👇
|
|
83
|
+
|
|
84
|
+
</center>
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"data": [
|
|
89
|
+
{
|
|
90
|
+
"name": "The Flash",
|
|
91
|
+
"secret_identity": "Barry Allen",
|
|
92
|
+
"id": 11
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "Green Lantern",
|
|
96
|
+
"secret_identity": "Hal Jordan",
|
|
97
|
+
"id": 12
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"meta": {
|
|
101
|
+
"offset": 10,
|
|
102
|
+
"total_items": 12,
|
|
103
|
+
"total_pages": 2,
|
|
104
|
+
"page_number": 2
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
* Pagination customization:
|
|
110
|
+
```python
|
|
111
|
+
...
|
|
112
|
+
from fastapi import Page, new_pagination
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
Paginate = new_pagination(min_page_size=5, max_page_size=500)
|
|
116
|
+
|
|
117
|
+
@app.get("/heros", response_model=Page[HeroModel])
|
|
118
|
+
async def get_heros(paginate:Paginate):
|
|
119
|
+
return paginate(select(Hero))
|
|
120
|
+
```
|
|
121
|
+
* Session lifecycle management: session is commited on request success or rollback on
|
|
122
|
+
failure.
|
|
123
|
+
|
|
124
|
+
* [`SQLModel`](http://sqlmodel.tiangolo.com/) support:
|
|
125
|
+
```python
|
|
126
|
+
...
|
|
127
|
+
from fastsqla import Item, Page, Paginate, Session
|
|
128
|
+
from sqlmodel import Field, SQLModel
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
class Hero(SQLModel, table=True):
|
|
132
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
133
|
+
name: str
|
|
134
|
+
secret_identity: str
|
|
135
|
+
age: int
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.get("/heroes", response_model=Page[Hero])
|
|
139
|
+
async def get_heroes(paginate: Paginate):
|
|
140
|
+
return await paginate(select(Hero))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.get("/heroes/{hero_id}", response_model=Item[Hero])
|
|
144
|
+
async def get_hero(session: Session, hero_id: int):
|
|
145
|
+
hero = await session.get(Hero, hero_id)
|
|
146
|
+
if hero is None:
|
|
147
|
+
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
|
148
|
+
return {"data": hero}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Installing
|
|
152
|
+
|
|
153
|
+
Using [uv](https://docs.astral.sh/uv/):
|
|
154
|
+
```bash
|
|
155
|
+
uv add fastsqla
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Using [pip](https://pip.pypa.io/):
|
|
159
|
+
```
|
|
160
|
+
pip install fastsqla
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Quick Example
|
|
164
|
+
|
|
165
|
+
### `example.py`
|
|
166
|
+
|
|
167
|
+
Let's write some tiny app in `example.py`:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# example.py
|
|
171
|
+
from http import HTTPStatus
|
|
172
|
+
|
|
173
|
+
from fastapi import FastAPI, HTTPException
|
|
174
|
+
from fastsqla import Base, Item, Page, Paginate, Session, lifespan
|
|
175
|
+
from pydantic import BaseModel, ConfigDict
|
|
176
|
+
from sqlalchemy import select
|
|
177
|
+
from sqlalchemy.exc import IntegrityError
|
|
178
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
app = FastAPI(lifespan=lifespan)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Hero(Base):
|
|
185
|
+
__tablename__ = "hero"
|
|
186
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
187
|
+
name: Mapped[str] = mapped_column(unique=True)
|
|
188
|
+
secret_identity: Mapped[str]
|
|
189
|
+
age: Mapped[int]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class HeroBase(BaseModel):
|
|
193
|
+
name: str
|
|
194
|
+
secret_identity: str
|
|
195
|
+
age: int
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class HeroModel(HeroBase):
|
|
199
|
+
model_config = ConfigDict(from_attributes=True)
|
|
200
|
+
id: int
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.get("/heros", response_model=Page[HeroModel])
|
|
204
|
+
async def list_heros(paginate: Paginate):
|
|
205
|
+
stmt = select(Hero)
|
|
206
|
+
return await paginate(stmt)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.get("/heros/{hero_id}", response_model=Item[HeroModel])
|
|
210
|
+
async def get_hero(hero_id: int, session: Session):
|
|
211
|
+
hero = await session.get(Hero, hero_id)
|
|
212
|
+
if hero is None:
|
|
213
|
+
raise HTTPException(HTTPStatus.NOT_FOUND, "Hero not found")
|
|
214
|
+
return {"data": hero}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@app.post("/heros", response_model=Item[HeroModel])
|
|
218
|
+
async def create_hero(new_hero: HeroBase, session: Session):
|
|
219
|
+
hero = Hero(**new_hero.model_dump())
|
|
220
|
+
session.add(hero)
|
|
221
|
+
try:
|
|
222
|
+
await session.flush()
|
|
223
|
+
except IntegrityError:
|
|
224
|
+
raise HTTPException(HTTPStatus.CONFLICT, "Duplicate hero name")
|
|
225
|
+
return {"data": hero}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Database
|
|
229
|
+
|
|
230
|
+
💡 This example uses an `SQLite` database for simplicity: `FastSQLA` is compatible with
|
|
231
|
+
all asynchronous db drivers that `SQLAlchemy` is compatible with.
|
|
232
|
+
|
|
233
|
+
Let's create an `SQLite` database using `sqlite3` and insert 12 rows in the `hero` table:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
sqlite3 db.sqlite <<EOF
|
|
237
|
+
-- Create Table hero
|
|
238
|
+
CREATE TABLE hero (
|
|
239
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
240
|
+
name TEXT NOT NULL UNIQUE, -- Unique hero name (e.g., Superman)
|
|
241
|
+
secret_identity TEXT NOT NULL, -- Secret identity (e.g., Clark Kent)
|
|
242
|
+
age INTEGER NOT NULL -- Age of the hero (e.g., 30)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
-- Insert heroes with their name, secret identity, and age
|
|
246
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Superman', 'Clark Kent', 30);
|
|
247
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Batman', 'Bruce Wayne', 35);
|
|
248
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Wonder Woman', 'Diana Prince', 30);
|
|
249
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Iron Man', 'Tony Stark', 45);
|
|
250
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Spider-Man', 'Peter Parker', 25);
|
|
251
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Captain America', 'Steve Rogers', 100);
|
|
252
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Black Widow', 'Natasha Romanoff', 35);
|
|
253
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Thor', 'Thor Odinson', 1500);
|
|
254
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Scarlet Witch', 'Wanda Maximoff', 30);
|
|
255
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Doctor Strange', 'Stephen Strange', 40);
|
|
256
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('The Flash', 'Barry Allen', 28);
|
|
257
|
+
INSERT INTO hero (name, secret_identity, age) VALUES ('Green Lantern', 'Hal Jordan', 35);
|
|
258
|
+
EOF
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Run the app
|
|
262
|
+
|
|
263
|
+
Let's install required dependencies:
|
|
264
|
+
```bash
|
|
265
|
+
pip install uvicorn aiosqlite fastsqla
|
|
266
|
+
```
|
|
267
|
+
Let's run the app:
|
|
268
|
+
```
|
|
269
|
+
sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false \
|
|
270
|
+
uvicorn example:app
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Check the result
|
|
274
|
+
|
|
275
|
+
Execute `GET /heros?offset=10&limit=10` using `curl`:
|
|
276
|
+
```bash
|
|
277
|
+
curl -X 'GET' -H 'accept: application/json' 'http://127.0.0.1:8000/heros?offset=10&limit=10'
|
|
278
|
+
```
|
|
279
|
+
Returns:
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"data": [
|
|
283
|
+
{
|
|
284
|
+
"name": "The Flash",
|
|
285
|
+
"secret_identity": "Barry Allen",
|
|
286
|
+
"id": 11
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "Green Lantern",
|
|
290
|
+
"secret_identity": "Hal Jordan",
|
|
291
|
+
"id": 12
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"meta": {
|
|
295
|
+
"offset": 10,
|
|
296
|
+
"total_items": 12,
|
|
297
|
+
"total_pages": 2,
|
|
298
|
+
"page_number": 2
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
You can also check the generated openapi doc by opening your browser to
|
|
304
|
+
[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).
|
|
305
|
+
|
|
306
|
+

|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
This project is licensed under the terms of the [MIT license](https://github.com/hadrien/FastSQLA/blob/main/LICENSE).
|