1- # 🚀 FastSQLA
1+ # FastSQLA
2+
3+ _ Async SQLAlchemy 2 for FastAPI — boilerplate, pagination, and seamless session management._
24
35[ ![ PyPI - Version] ( https://img.shields.io/pypi/v/FastSQLA?color=brightgreen )] ( https://pypi.org/project/FastSQLA/ )
6+ [ ![ 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 )
7+ [ ![ Codecov] ( https://img.shields.io/codecov/c/github/hadrien/fastsqla?token=XK3YT60MWK&logo=codecov )] ( https://codecov.io/gh/hadrien/FastSQLA )
48[ ![ Conventional Commits] ( https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg )] ( https://conventionalcommits.org )
5- [ ![ codecov] ( https://codecov.io/gh/hadrien/fastsqla/graph/badge.svg?token=XK3YT60MWK )] ( https://codecov.io/gh/hadrien/fastsqla )
9+ [ ![ GitHub License] ( https://img.shields.io/github/license/hadrien/fastsqla )] ( https://github.com/hadrien/FastSQLA/blob/main/LICENSE )
10+ ![ 🍁 With love from Canada] ( https://img.shields.io/badge/With%20love%20from%20Canada-ffffff?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIiB2aWV3Qm94PSItMjAxNSAtMjAwMCA0MDMwIDQwMzAiPjxwYXRoIGZpbGw9IiNmMDAiIGQ9Im0tOTAgMjAzMCA0NS04NjNhOTUgOTUgMCAwIDAtMTExLTk4bC04NTkgMTUxIDExNi0zMjBhNjUgNjUgMCAwIDAtMjAtNzNsLTk0MS03NjIgMjEyLTk5YTY1IDY1IDAgMCAwIDM0LTc5bC0xODYtNTcyIDU0MiAxMTVhNjUgNjUgMCAwIDAgNzMtMzhsMTA1LTI0NyA0MjMgNDU0YTY1IDY1IDAgMCAwIDExMS01N2wtMjA0LTEwNTIgMzI3IDE4OWE2NSA2NSAwIDAgMCA5MS0yN2wzMzItNjUyIDMzMiA2NTJhNjUgNjUgMCAwIDAgOTEgMjdsMzI3LTE4OS0yMDQgMTA1MmE2NSA2NSAwIDAgMCAxMTEgNTdsNDIzLTQ1NCAxMDUgMjQ3YTY1IDY1IDAgMCAwIDczIDM4bDU0Mi0xMTUtMTg2IDU3MmE2NSA2NSAwIDAgMCAzNCA3OWwyMTIgOTktOTQxIDc2MmE2NSA2NSAwIDAgMC0yMCA3M2wxMTYgMzIwLTg1OS0xNTFhOTUgOTUgMCAwIDAtMTExIDk4bDQ1IDg2M3oiLz48L3N2Zz4K )
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 [ ` SQLAlchemy 2 ` ] ( https://docs.sqlalchemy.org/en/20/ ) extension for
19+ [ ` FastAPI ` ] ( https://fastapi.tiangolo.com/ ) .
20+ It streamlines the configuration and asynchronous connection to relational databases by
21+ providing boilerplate and intuitive helpers. Additionally, it offers built-in
22+ customizable pagination and automatically manages the ` SQLAlchemy ` session lifecycle
23+ 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 ) .
624
7- ` FastSQLA ` is an [ ` SQLAlchemy ` ] extension for [ ` FastAPI ` ] .
8- It supports asynchronous ` SQLAlchemy ` sessions and includes built-in custimizable
9- pagination.
1025
1126## Features
1227
13- < details >
14- <summary>Automatic SQLAlchemy configuration at app startup.</summary>
28+ * Easy setup at app startup using
29+ [ ` FastAPI ` Lifespan ] ( https://fastapi.tiangolo.com/advanced/events/#lifespan ) :
1530
16- Using [ ` FastAPI ` Lifespan] ( https://fastapi.tiangolo.com/advanced/events/#lifespan ) :
17- ``` python
18- from fastapi import FastAPI
19- from fastsqla import lifespan
31+ ``` python
32+ from fastapi import FastAPI
33+ from fastsqla import lifespan
2034
21- app = FastAPI(lifespan = lifespan)
22- ```
23- </details >
24- <details >
25- <summary>Async SQLAlchemy session as a FastAPI dependency.</summary>
35+ app = FastAPI(lifespan = lifespan)
36+ ```
2637
27- ``` python
28- ...
29- from fastsqla import Session
30- from sqlalchemy import select
31- ...
38+ * `SQLAlchemy` async session dependency:
3239
33- @app.get ( " /heros " )
34- async def get_heros ( session :Session):
35- stmt = select( ... )
36- result = await session.execute(stmt)
40+ ```python
41+ ...
42+ from fastsqla import Session
43+ from sqlalchemy import select
3744 ...
38- ```
39- </details >
40- <details >
41- <summary>Built-in pagination.</summary>
4245
43- ``` python
44- ...
45- from fastsqla import Page, Paginate
46- from sqlalchemy import select
47- ...
46+ @app.get (" /heros" )
47+ async def get_heros (session :Session):
48+ stmt = select(... )
49+ result = await session.execute(stmt)
50+ ...
51+ ```
4852
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>
53+ * `SQLAlchemy` async session with an async context manager:
5654
57- ``` python
58- ...
59- from fastapi import Page, new_pagination
60- ...
55+ ```python
56+ from fastsqla import open_session
6157
62- Paginate = new_pagination(min_page_size = 5 , max_page_size = 500 )
58+ async def background_job ():
59+ async with open_session() as session:
60+ stmt = select(... )
61+ result = await session.execute(stmt)
62+ ...
63+ ```
6364
64- @app.get (" /heros" , response_model = Page[HeroModel])
65- async def get_heros (paginate :Paginate):
66- return paginate(select(Hero))
67- ```
68- </details >
65+ * Built- in pagination:
66+
67+ ```python
68+ ...
69+ from fastsqla import Page, Paginate
70+ from sqlalchemy import select
71+ ...
72+
73+ @app.get (" /heros" , response_model = Page[HeroModel])
74+ async def get_heros (paginate :Paginate):
75+ return await paginate(select(Hero))
76+ ```
77+ < center> 👇👇👇< / center>
78+ ```json
79+ // / heros? offset= 10 & limit= 10
80+ {
81+ " data" : [
82+ {
83+ " name" : " The Flash" ,
84+ " secret_identity" : " Barry Allen" ,
85+ " id" : 11
86+ },
87+ {
88+ " name" : " Green Lantern" ,
89+ " secret_identity" : " Hal Jordan" ,
90+ " id" : 12
91+ }
92+ ],
93+ " meta" : {
94+ " offset" : 10 ,
95+ " total_items" : 12 ,
96+ " total_pages" : 2 ,
97+ " page_number" : 2
98+ }
99+ }
100+ ```
101+
102+ * Pagination customization:
103+ ```python
104+ ...
105+ from fastapi import Page, new_pagination
106+ ...
107+
108+ Paginate = new_pagination(min_page_size = 5 , max_page_size = 500 )
109+
110+ @app.get (" /heros" , response_model = Page[HeroModel])
111+ async def get_heros (paginate :Paginate):
112+ return paginate(select(Hero))
113+ ```
114+ * Session lifecycle management: session is commited on request success or rollback on
115+ failure.
69116
70- And more ...
71- <!-- <details><summary></summary></details> -->
72117
73118# # Installing
74119
@@ -84,17 +129,21 @@ pip install fastsqla
84129
85130## Quick Example
86131
132+ ### ` example.py `
133+
134+ Let's write some tiny app in ` example.py ` :
135+
87136``` python
88137# example.py
89138from http import HTTPStatus
90139
91140from fastapi import FastAPI, HTTPException
141+ from fastsqla import Base, Item, Page, Paginate, Session, lifespan
92142from pydantic import BaseModel, ConfigDict
93143from sqlalchemy import select
94144from sqlalchemy.exc import IntegrityError
95145from sqlalchemy.orm import Mapped, mapped_column
96146
97- from fastsqla import Base, Item, Page, Paginate, Session, lifespan
98147
99148app = FastAPI(lifespan = lifespan)
100149
@@ -104,11 +153,13 @@ class Hero(Base):
104153 id : Mapped[int ] = mapped_column(primary_key = True )
105154 name: Mapped[str ] = mapped_column(unique = True )
106155 secret_identity: Mapped[str ]
156+ age: Mapped[int ]
107157
108158
109159class HeroBase (BaseModel ):
110160 name: str
111161 secret_identity: str
162+ age: int
112163
113164
114165class HeroModel (HeroBase ):
@@ -117,20 +168,21 @@ class HeroModel(HeroBase):
117168
118169
119170@app.get (" /heros" , response_model = Page[HeroModel])
120- async def list_users (paginate : Paginate):
121- return await paginate(select(Hero))
171+ async def list_heros (paginate : Paginate):
172+ stmt = select(Hero)
173+ return await paginate(stmt)
122174
123175
124176@app.get (" /heros/{hero_id} " , response_model = Item[HeroModel])
125- async def get_user (hero_id : int , session : Session):
177+ async def get_hero (hero_id : int , session : Session):
126178 hero = await session.get(Hero, hero_id)
127179 if hero is None :
128180 raise HTTPException(HTTPStatus.NOT_FOUND , " Hero not found" )
129181 return {" data" : hero}
130182
131183
132184@app.post (" /heros" , response_model = Item[HeroModel])
133- async def create_user (new_hero : HeroBase, session : Session):
185+ async def create_hero (new_hero : HeroBase, session : Session):
134186 hero = Hero(** new_hero.model_dump())
135187 session.add(hero)
136188 try :
@@ -140,55 +192,56 @@ async def create_user(new_hero: HeroBase, session: Session):
140192 return {" data" : hero}
141193```
142194
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.
195+ ### Database
196+
197+ 💡 This example uses an ` SQLite ` database for simplicity: ` FastSQLA ` is compatible with
198+ all asynchronous db drivers that ` SQLAlchemy ` is compatible with.
146199
147- <details >
148- <summary>Create an <code>sqlite3</code> db:</summary>
200+ Let's create an ` SQLite ` database using ` sqlite3 ` and insert 12 rows in the ` hero ` table:
149201
150202``` bash
151203sqlite3 db.sqlite << EOF
204+ -- Create Table hero
152205CREATE TABLE hero (
153206 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)
207+ name TEXT NOT NULL UNIQUE, -- Unique hero name (e.g., Superman)
208+ secret_identity TEXT NOT NULL, -- Secret identity (e.g., Clark Kent)
209+ age INTEGER NOT NULL -- Age of the hero (e.g., 30)
156210);
157211
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');
212+ -- Insert heroes with their name, secret identity, and age
213+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Superman', 'Clark Kent', 30 );
214+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Batman', 'Bruce Wayne', 35 );
215+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Wonder Woman', 'Diana Prince', 30 );
216+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Iron Man', 'Tony Stark', 45 );
217+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Spider-Man', 'Peter Parker', 25 );
218+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Captain America', 'Steve Rogers', 100 );
219+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Black Widow', 'Natasha Romanoff', 35 );
220+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Thor', 'Thor Odinson', 1500 );
221+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Scarlet Witch', 'Wanda Maximoff', 30 );
222+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Doctor Strange', 'Stephen Strange', 40 );
223+ INSERT INTO hero (name, secret_identity, age ) VALUES ('The Flash', 'Barry Allen', 28 );
224+ INSERT INTO hero (name, secret_identity, age ) VALUES ('Green Lantern', 'Hal Jordan', 35 );
171225EOF
172226```
173227
174- </details >
175-
176- <details >
177- <summary>Install dependencies & run the app</summary>
228+ ### Run the app
178229
230+ Let's install required dependencies:
179231``` bash
180232pip install uvicorn aiosqlite fastsqla
181- sqlalchemy_url=sqlite+aiosqlite:///db.sqlite? check_same_thread=false uvicorn example:app
233+ ```
234+ Let's run the app:
235+ ```
236+ sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false \
237+ uvicorn example:app
182238```
183239
184- </details >
185-
186- Execute ` GET /heros?offset=10 ` :
240+ ### Check the result
187241
242+ Execute ` GET /heros?offset=10&limit=10 ` using ` curl ` :
188243``` bash
189- curl -X ' GET' \
190- ' http://127.0.0.1:8000/heros?offset=10&limit=10' \
191- -H ' accept: application/json'
244+ curl -X ' GET' -H ' accept: application/json' ' http://127.0.0.1:8000/heros?offset=10&limit=10'
192245```
193246Returns:
194247``` json
@@ -214,5 +267,11 @@ Returns:
214267}
215268```
216269
217- [ `FastAPI` ] : https://fastapi.tiangolo.com/
218- [ `SQLAlchemy` ] : http://sqlalchemy.org/
270+ You can also check the generated openapi doc by opening your browser to
271+ [ http://127.0.0.1:8000/docs ] ( http://127.0.0.1:8000/docs ) .
272+
273+ ![ OpenAPI generated documentation of the example API] ( images/example-openapi-generated-doc.png )
274+
275+ ## License
276+
277+ This project is licensed under the terms of the [ MIT license] ( https://github.com/hadrien/FastSQLA/blob/main/LICENSE ) .
0 commit comments