|
1 | | -# FastAPI-Async-SQLA |
| 1 | +# 🚀 FastSQLA |
2 | 2 |
|
3 | | -[](https://pypi.org/project/FastAPI-Async-SQLA/) |
| 3 | +[](https://pypi.org/project/FastSQLA/) |
4 | 4 | [](https://conventionalcommits.org) |
5 | | -[](https://codecov.io/gh/hadrien/fastapi-async-sqla) |
| 5 | +[](https://codecov.io/gh/hadrien/fastsqla) |
6 | 6 |
|
7 | | -FastAPI-Async-SQLA is an [SQLAlchemy] extension for [FastAPI]. It supports asynchronous |
8 | | -SQLAlchemy sessions using SQLAlchemy >= 2.0 and provides pagination support. |
| 7 | +`FastSQLA` is an [`SQLAlchemy`] extension for [`FastAPI`]. |
| 8 | +It supports asynchronous `SQLAlchemy` sessions and includes built-in custimizable |
| 9 | +pagination. |
9 | 10 |
|
10 | | -# Installing |
| 11 | +## Features |
11 | 12 |
|
12 | | -Using [pip](https://pip.pypa.io/): |
| 13 | +<details> |
| 14 | + <summary>Automatic SQLAlchemy configuration at app startup.</summary> |
| 15 | + |
| 16 | + Using [`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan): |
| 17 | +```python |
| 18 | +from fastapi import FastAPI |
| 19 | +from fastsqla import lifespan |
| 20 | + |
| 21 | +app = FastAPI(lifespan=lifespan) |
| 22 | +``` |
| 23 | +</details> |
| 24 | +<details> |
| 25 | + <summary>Async SQLAlchemy session as a FastAPI dependency.</summary> |
| 26 | + |
| 27 | +```python |
| 28 | +... |
| 29 | +from fastsqla import Session |
| 30 | +from sqlalchemy import select |
| 31 | +... |
| 32 | + |
| 33 | +@app.get("/heros") |
| 34 | +async def get_heros(session:Session): |
| 35 | + stmt = select(...) |
| 36 | + result = await session.execute(stmt) |
| 37 | + ... |
| 38 | +``` |
| 39 | +</details> |
| 40 | +<details> |
| 41 | + <summary>Built-in pagination.</summary> |
| 42 | + |
| 43 | +```python |
| 44 | +... |
| 45 | +from fastsqla import Page, Paginate |
| 46 | +from sqlalchemy import select |
| 47 | +... |
| 48 | + |
| 49 | +@app.get("/heros", response_model=Page[HeroModel]) |
| 50 | +async def get_heros(paginate:Paginate): |
| 51 | + return paginate(select(Hero)) |
| 52 | +``` |
| 53 | +</details> |
| 54 | +<details> |
| 55 | + <summary>Allows pagination customization.</summary> |
| 56 | + |
| 57 | +```python |
| 58 | +... |
| 59 | +from fastapi import new_pagination |
| 60 | +... |
| 61 | + |
| 62 | +Paginate = new_pagination(min_page_size=5, max_page_size=500) |
| 63 | + |
| 64 | +@app.get("/heros", response_model=Page[HeroModel]) |
| 65 | +async def get_heros(paginate:Paginate): |
| 66 | + return paginate(select(Hero)) |
13 | 67 | ``` |
14 | | -pip install fastapi-async-sqla |
| 68 | +</details> |
| 69 | + |
| 70 | +And more ... |
| 71 | +<!-- <details><summary></summary></details> --> |
| 72 | + |
| 73 | +## Installing |
| 74 | + |
| 75 | +Using [uv](https://docs.astral.sh/uv/): |
| 76 | +```bash |
| 77 | +uv add fastsqla |
15 | 78 | ``` |
16 | 79 |
|
17 | | -# Quick Example |
| 80 | +Using [pip](https://pip.pypa.io/): |
| 81 | +``` |
| 82 | +pip install fastsqla |
| 83 | +``` |
18 | 84 |
|
19 | | -Assuming it runs against a DB with a table `user` with 2 columns `id` and `name`: |
| 85 | +## Quick Example |
20 | 86 |
|
21 | 87 | ```python |
22 | | -# main.py |
| 88 | +# example.py |
| 89 | +from http import HTTPStatus |
| 90 | + |
23 | 91 | from fastapi import FastAPI, HTTPException |
24 | | -from fastapi_async_sqla import Base, Item, Page, Paginate, Session, lifespan |
25 | | -from pydantic import BaseModel |
| 92 | +from pydantic import BaseModel, ConfigDict |
26 | 93 | from sqlalchemy import select |
| 94 | +from sqlalchemy.exc import IntegrityError |
| 95 | +from sqlalchemy.orm import Mapped, mapped_column |
27 | 96 |
|
| 97 | +from fastsqla import Base, Item, Page, Paginate, Session, lifespan |
28 | 98 |
|
29 | 99 | app = FastAPI(lifespan=lifespan) |
30 | 100 |
|
31 | 101 |
|
32 | | -class User(Base): |
33 | | - __tablename__ = "user" |
| 102 | +class Hero(Base): |
| 103 | + __tablename__ = "hero" |
| 104 | + id: Mapped[int] = mapped_column(primary_key=True) |
| 105 | + name: Mapped[str] = mapped_column(unique=True) |
| 106 | + secret_identity: Mapped[str] |
34 | 107 |
|
35 | 108 |
|
36 | | -class UserIn(BaseModel): |
| 109 | +class HeroBase(BaseModel): |
37 | 110 | name: str |
| 111 | + secret_identity: str |
38 | 112 |
|
39 | 113 |
|
40 | | -class UserModel(UserIn): |
| 114 | +class HeroModel(HeroBase): |
| 115 | + model_config = ConfigDict(from_attributes=True) |
41 | 116 | id: int |
42 | 117 |
|
43 | 118 |
|
44 | | -@app.get("/users", response_model=Page[UserModel]) |
| 119 | +@app.get("/heros", response_model=Page[HeroModel]) |
45 | 120 | async def list_users(paginate: Paginate): |
46 | | - return await paginate(select(User)) |
47 | | - |
48 | | - |
49 | | -@app.get("/users/{user_id}", response_model=Item[UserModel]) |
50 | | -async def get_user(user_id: int, session: Session): |
51 | | - user = await session.get(User, user_id) |
52 | | - if user is None: |
53 | | - raise HTTPException(404) |
54 | | - return {"data": user} |
| 121 | + return await paginate(select(Hero)) |
| 122 | + |
| 123 | + |
| 124 | +@app.get("/heros/{hero_id}", response_model=Item[HeroModel]) |
| 125 | +async def get_user(hero_id: int, session: Session): |
| 126 | + hero = await session.get(Hero, hero_id) |
| 127 | + if hero is None: |
| 128 | + raise HTTPException(HTTPStatus.NOT_FOUND, "Hero not found") |
| 129 | + return {"data": hero} |
| 130 | + |
| 131 | + |
| 132 | +@app.post("/heros", response_model=Item[HeroModel]) |
| 133 | +async def create_user(new_hero: HeroBase, session: Session): |
| 134 | + hero = Hero(**new_hero.model_dump()) |
| 135 | + session.add(hero) |
| 136 | + try: |
| 137 | + await session.flush() |
| 138 | + except IntegrityError: |
| 139 | + raise HTTPException(HTTPStatus.CONFLICT, "Duplicate hero name") |
| 140 | + return {"data": hero} |
| 141 | +``` |
55 | 142 |
|
| 143 | +> [!NOTE] |
| 144 | +> Sqlite is used for the sake of the example. |
| 145 | +> FastSQLA is compatible with all async db drivers that SQLAlchemy is compatible with. |
56 | 146 |
|
57 | | -@app.post("/users", response_model=Item[UserModel]) |
58 | | -async def create_user(new_user: UserIn, session: Session): |
59 | | - user = User(**new_user.model_dump()) |
60 | | - session.add(user) |
61 | | - await session.flush() |
62 | | - return {"data": user} |
63 | | -``` |
| 147 | +<details> |
| 148 | + <summary>Create an <code>sqlite3</code> db:</summary> |
64 | 149 |
|
65 | | -Creating a db using `sqlite3`: |
66 | 150 | ```bash |
67 | 151 | sqlite3 db.sqlite <<EOF |
68 | | -CREATE TABLE user ( |
69 | | - id INTEGER PRIMARY KEY AUTOINCREMENT, |
70 | | - name TEXT NOT NULL |
| 152 | +CREATE TABLE hero ( |
| 153 | + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 154 | + name TEXT NOT NULL UNIQUE, -- Hero name (e.g., Superman) |
| 155 | + secret_identity TEXT NOT NULL -- Secret identity (e.g., Clark Kent) |
71 | 156 | ); |
| 157 | +
|
| 158 | +-- Insert heroes with hero name and secret identity |
| 159 | +INSERT INTO hero (name, secret_identity) VALUES ('Superman', 'Clark Kent'); |
| 160 | +INSERT INTO hero (name, secret_identity) VALUES ('Batman', 'Bruce Wayne'); |
| 161 | +INSERT INTO hero (name, secret_identity) VALUES ('Wonder Woman', 'Diana Prince'); |
| 162 | +INSERT INTO hero (name, secret_identity) VALUES ('Iron Man', 'Tony Stark'); |
| 163 | +INSERT INTO hero (name, secret_identity) VALUES ('Spider-Man', 'Peter Parker'); |
| 164 | +INSERT INTO hero (name, secret_identity) VALUES ('Captain America', 'Steve Rogers'); |
| 165 | +INSERT INTO hero (name, secret_identity) VALUES ('Black Widow', 'Natasha Romanoff'); |
| 166 | +INSERT INTO hero (name, secret_identity) VALUES ('Thor', 'Thor Odinson'); |
| 167 | +INSERT INTO hero (name, secret_identity) VALUES ('Scarlet Witch', 'Wanda Maximoff'); |
| 168 | +INSERT INTO hero (name, secret_identity) VALUES ('Doctor Strange', 'Stephen Strange'); |
| 169 | +INSERT INTO hero (name, secret_identity) VALUES ('The Flash', 'Barry Allen'); |
| 170 | +INSERT INTO hero (name, secret_identity) VALUES ('Green Lantern', 'Hal Jordan'); |
72 | 171 | EOF |
73 | 172 | ``` |
74 | 173 |
|
75 | | -Installing [aiosqlite] to connect to the sqlite db asynchronously: |
| 174 | +</details> |
| 175 | + |
| 176 | +<details> |
| 177 | + <summary>Install dependencies & run the app</summary> |
| 178 | + |
76 | 179 | ```bash |
77 | | -pip install aiosqlite |
| 180 | +pip install uvicorn aiosqlite fastsqla |
| 181 | +sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false uvicorn example:app |
78 | 182 | ``` |
79 | 183 |
|
80 | | -Running the app: |
| 184 | +</details> |
| 185 | + |
| 186 | +Execute `GET /heros?offset=10`: |
| 187 | + |
81 | 188 | ```bash |
82 | | -sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false uvicorn main:app |
| 189 | +curl -X 'GET' \ |
| 190 | +'http://127.0.0.1:8000/heros?offset=10&limit=10' \ |
| 191 | +-H 'accept: application/json' |
| 192 | +``` |
| 193 | +Returns: |
| 194 | +```json |
| 195 | +{ |
| 196 | + "data": [ |
| 197 | + { |
| 198 | + "name": "The Flash", |
| 199 | + "secret_identity": "Barry Allen", |
| 200 | + "id": 11 |
| 201 | + }, |
| 202 | + { |
| 203 | + "name": "Green Lantern", |
| 204 | + "secret_identity": "Hal Jordan", |
| 205 | + "id": 12 |
| 206 | + } |
| 207 | + ], |
| 208 | + "meta": { |
| 209 | + "offset": 10, |
| 210 | + "total_items": 12, |
| 211 | + "total_pages": 2, |
| 212 | + "page_number": 2 |
| 213 | + } |
| 214 | +} |
83 | 215 | ``` |
84 | 216 |
|
85 | | -[aiosqlite]: https://github.com/omnilib/aiosqlite |
86 | | -[FastAPI]: https://fastapi.tiangolo.com/ |
87 | | -[SQLAlchemy]: http://sqlalchemy.org/ |
| 217 | +[`FastAPI`]: https://fastapi.tiangolo.com/ |
| 218 | +[`SQLAlchemy`]: http://sqlalchemy.org/ |
0 commit comments