Skip to content

Commit af90cc5

Browse files
authored
docs: change project name & write a better README (#10)
1 parent db4c605 commit af90cc5

File tree

12 files changed

+256
-105
lines changed

12 files changed

+256
-105
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: 🔧 setup uv
1414
uses: ./.github/uv
1515
- name: 🧪 pytest
16-
run: uv run pytest --cov fastapi_async_sqla --cov-report=term-missing --cov-report=xml
16+
run: uv run pytest --cov fastsqla --cov-report=term-missing --cov-report=xml
1717
- name: "🐔 codecov: upload test coverage"
1818
uses: codecov/codecov-action@v4.2.0
1919
env:

README.md

Lines changed: 176 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,218 @@
1-
# FastAPI-Async-SQLA
1+
# 🚀 FastSQLA
22

3-
[![PyPI - Version](https://img.shields.io/pypi/v/FastAPI-Async-SQLA?color=brightgreen)](https://pypi.org/project/FastAPI-Async-SQLA/)
3+
[![PyPI - Version](https://img.shields.io/pypi/v/FastSQLA?color=brightgreen)](https://pypi.org/project/FastSQLA/)
44
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg)](https://conventionalcommits.org)
5-
[![codecov](https://codecov.io/gh/hadrien/fastapi-async-sqla/graph/badge.svg?token=XK3YT60MWK)](https://codecov.io/gh/hadrien/fastapi-async-sqla)
5+
[![codecov](https://codecov.io/gh/hadrien/fastsqla/graph/badge.svg?token=XK3YT60MWK)](https://codecov.io/gh/hadrien/fastsqla)
66

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.
910

10-
# Installing
11+
## Features
1112

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))
1367
```
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
1578
```
1679

17-
# Quick Example
80+
Using [pip](https://pip.pypa.io/):
81+
```
82+
pip install fastsqla
83+
```
1884

19-
Assuming it runs against a DB with a table `user` with 2 columns `id` and `name`:
85+
## Quick Example
2086

2187
```python
22-
# main.py
88+
# example.py
89+
from http import HTTPStatus
90+
2391
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
2693
from sqlalchemy import select
94+
from sqlalchemy.exc import IntegrityError
95+
from sqlalchemy.orm import Mapped, mapped_column
2796

97+
from fastsqla import Base, Item, Page, Paginate, Session, lifespan
2898

2999
app = FastAPI(lifespan=lifespan)
30100

31101

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]
34107

35108

36-
class UserIn(BaseModel):
109+
class HeroBase(BaseModel):
37110
name: str
111+
secret_identity: str
38112

39113

40-
class UserModel(UserIn):
114+
class HeroModel(HeroBase):
115+
model_config = ConfigDict(from_attributes=True)
41116
id: int
42117

43118

44-
@app.get("/users", response_model=Page[UserModel])
119+
@app.get("/heros", response_model=Page[HeroModel])
45120
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+
```
55142

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.
56146
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>
64149

65-
Creating a db using `sqlite3`:
66150
```bash
67151
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)
71156
);
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');
72171
EOF
73172
```
74173

75-
Installing [aiosqlite] to connect to the sqlite db asynchronously:
174+
</details>
175+
176+
<details>
177+
<summary>Install dependencies & run the app</summary>
178+
76179
```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
78182
```
79183

80-
Running the app:
184+
</details>
185+
186+
Execute `GET /heros?offset=10`:
187+
81188
```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+
}
83215
```
84216

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/

pyproject.toml

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
2-
name = "FastAPI-Async-SQLA"
2+
name = "FastSQLA"
33
version = "0.2.3"
4-
description = "SQLAlchemy extension for FastAPI with support for asynchronous SQLAlchemy sessions and pagination."
4+
description = "SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination."
55
readme = "README.md"
66
requires-python = ">=3.12"
77
authors = [{ name = "Hadrien David", email = "bonjour@hadriendavid.com" }]
@@ -33,18 +33,14 @@ classifiers = [
3333
]
3434
keywords = ["FastAPI", "SQLAlchemy", "AsyncIO"]
3535
license = { text = "MIT License" }
36-
dependencies = [
37-
"fastapi>=0.115.6",
38-
"sqlalchemy[asyncio]>=2.0.34,<3",
39-
"structlog>=24.4.0",
40-
]
36+
dependencies = ["fastapi>=0.115.6", "sqlalchemy[asyncio]>=2.0.37", "structlog>=24.4.0"]
4137

4238
[project.urls]
43-
Homepage = "https://github.com/hadrien/fastapi-async-sqla"
44-
Documentation = "https://github.com/hadrien/fastapi-async-sqla"
45-
Repository = "https://github.com/hadrien/fastapi-async-sqla"
46-
Issues = "https://github.com/hadrien/fastapi-async-sqla/issues"
47-
Changelog = "https://github.com/hadrien/fastapi-async-sqla/releases"
39+
Homepage = "https://github.com/hadrien/fastsqla"
40+
Documentation = "https://github.com/hadrien/fastsqla"
41+
Repository = "https://github.com/hadrien/fastsqla"
42+
Issues = "https://github.com/hadrien/fastsqla/issues"
43+
Changelog = "https://github.com/hadrien/fastsqla/releases"
4844

4945
[tool.uv]
5046
package = true
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Base(DeclarativeBase, DeferredReflection):
4040

4141

4242
class State(TypedDict):
43-
fastapi_async_sqla_engine: AsyncEngine
43+
fastsqla_engine: AsyncEngine
4444

4545

4646
@asynccontextmanager
@@ -60,7 +60,7 @@ async def lifespan(_) -> AsyncGenerator[State, None]:
6060

6161
await logger.ainfo("Configured SQLAlchemy.")
6262

63-
yield {"fastapi_async_sqla_engine": engine}
63+
yield {"fastsqla_engine": engine}
6464

6565
SessionFactory.configure(bind=None)
6666
await engine.dispose()
@@ -120,7 +120,7 @@ class Collection(BaseModel, Generic[T]):
120120
data: list[T]
121121

122122

123-
class Page(Collection):
123+
class Page(Collection[T]):
124124
meta: Meta
125125

126126

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async def session(engine):
3232
def tear_down():
3333
from sqlalchemy.orm import clear_mappers
3434

35-
from fastapi_async_sqla import Base
35+
from fastsqla import Base
3636

3737
yield
3838

tests/integration/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
@fixture
88
def app(environ):
9-
from fastapi_async_sqla import lifespan
9+
from fastsqla import lifespan
1010

1111
app = FastAPI(lifespan=lifespan)
1212
return app

tests/integration/test_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async def setup_tear_down(engine):
1717

1818

1919
async def test_lifespan_reflects_user_table(environ):
20-
from fastapi_async_sqla import Base, lifespan
20+
from fastsqla import Base, lifespan
2121

2222
class User(Base):
2323
__tablename__ = "user"

tests/integration/test_pagination.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async def setup_tear_down(engine, faker):
5656

5757
@fixture
5858
def app(app):
59-
from fastapi_async_sqla import (
59+
from fastsqla import (
6060
Base,
6161
Page,
6262
Paginate,

tests/integration/test_session_dependency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async def setup_tear_down(engine):
2424

2525
@fixture
2626
def app(setup_tear_down, app):
27-
from fastapi_async_sqla import Base, Item, Session
27+
from fastsqla import Base, Item, Session
2828

2929
class User(Base):
3030
__tablename__ = "user"

0 commit comments

Comments
 (0)