FastSQLA 0.2.4__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.2.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hadrien David
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,261 @@
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/
@@ -0,0 +1,218 @@
1
+ # 🚀 FastSQLA
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/FastSQLA?color=brightgreen)](https://pypi.org/project/FastSQLA/)
4
+ [![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)
6
+
7
+ `FastSQLA` is an [`SQLAlchemy`] extension for [`FastAPI`].
8
+ It supports asynchronous `SQLAlchemy` sessions and includes built-in custimizable
9
+ pagination.
10
+
11
+ ## Features
12
+
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))
67
+ ```
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
78
+ ```
79
+
80
+ Using [pip](https://pip.pypa.io/):
81
+ ```
82
+ pip install fastsqla
83
+ ```
84
+
85
+ ## Quick Example
86
+
87
+ ```python
88
+ # example.py
89
+ from http import HTTPStatus
90
+
91
+ from fastapi import FastAPI, HTTPException
92
+ from pydantic import BaseModel, ConfigDict
93
+ from sqlalchemy import select
94
+ from sqlalchemy.exc import IntegrityError
95
+ from sqlalchemy.orm import Mapped, mapped_column
96
+
97
+ from fastsqla import Base, Item, Page, Paginate, Session, lifespan
98
+
99
+ app = FastAPI(lifespan=lifespan)
100
+
101
+
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]
107
+
108
+
109
+ class HeroBase(BaseModel):
110
+ name: str
111
+ secret_identity: str
112
+
113
+
114
+ class HeroModel(HeroBase):
115
+ model_config = ConfigDict(from_attributes=True)
116
+ id: int
117
+
118
+
119
+ @app.get("/heros", response_model=Page[HeroModel])
120
+ async def list_users(paginate: Paginate):
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
+ ```
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.
146
+
147
+ <details>
148
+ <summary>Create an <code>sqlite3</code> db:</summary>
149
+
150
+ ```bash
151
+ sqlite3 db.sqlite <<EOF
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)
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');
171
+ EOF
172
+ ```
173
+
174
+ </details>
175
+
176
+ <details>
177
+ <summary>Install dependencies & run the app</summary>
178
+
179
+ ```bash
180
+ pip install uvicorn aiosqlite fastsqla
181
+ sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false uvicorn example:app
182
+ ```
183
+
184
+ </details>
185
+
186
+ Execute `GET /heros?offset=10`:
187
+
188
+ ```bash
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
+ }
215
+ ```
216
+
217
+ [`FastAPI`]: https://fastapi.tiangolo.com/
218
+ [`SQLAlchemy`]: http://sqlalchemy.org/
@@ -0,0 +1,78 @@
1
+ [project]
2
+ name = "FastSQLA"
3
+ version = "0.2.4"
4
+ description = "SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ authors = [{ name = "Hadrien David", email = "bonjour@hadriendavid.com" }]
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Environment :: Web Environment",
11
+ "Framework :: AsyncIO",
12
+ "Framework :: FastAPI",
13
+ "Framework :: Pydantic :: 2",
14
+ "Framework :: Pydantic",
15
+ "Intended Audience :: Developers",
16
+ "Intended Audience :: Information Technology",
17
+ "Intended Audience :: System Administrators",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: SQL",
25
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
26
+ "Topic :: Internet :: WWW/HTTP",
27
+ "Topic :: Internet",
28
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Topic :: Software Development :: Libraries",
31
+ "Topic :: Software Development",
32
+ "Typing :: Typed",
33
+ ]
34
+ keywords = ["FastAPI", "SQLAlchemy", "AsyncIO"]
35
+ license = { text = "MIT License" }
36
+ dependencies = ["fastapi>=0.115.6", "sqlalchemy[asyncio]>=2.0.37", "structlog>=24.4.0"]
37
+
38
+ [project.urls]
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"
44
+
45
+ [tool.uv]
46
+ package = true
47
+ dev-dependencies = [
48
+ "asgi-lifespan>=2.1.0",
49
+ "coverage>=7.6.1",
50
+ "faker>=28.4.1",
51
+ "httpx>=0.27.2",
52
+ "pytest>=8.3.2",
53
+ "pytest-asyncio>=0.24.0",
54
+ "pytest-cov>=5.0.0",
55
+ "pytest-watch",
56
+ "ruff>=0.6.4",
57
+ "toml>=0.10.2",
58
+ "aiosqlite>=0.20.0",
59
+ "python-semantic-release>=9.8.8",
60
+ "twine>=5.1.1",
61
+ ]
62
+
63
+ [tool.uv.sources]
64
+ pytest-watch = { git = "https://github.com/styleseat/pytest-watch", rev = "0342193" }
65
+
66
+ [tool.pytest.ini_options]
67
+ asyncio_mode = 'auto'
68
+
69
+ [tool.coverage.run]
70
+ branch = true
71
+ omit = ["tests/*", ".venv/*"]
72
+ concurrency = ["thread", "greenlet"]
73
+
74
+ [tool.semantic_release.remote.token]
75
+ env = "GH_TOKEN"
76
+
77
+ [tool.semantic_release]
78
+ version_toml = ["pyproject.toml:project.version"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,261 @@
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/
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/fastsqla.py
5
+ src/FastSQLA.egg-info/PKG-INFO
6
+ src/FastSQLA.egg-info/SOURCES.txt
7
+ src/FastSQLA.egg-info/dependency_links.txt
8
+ src/FastSQLA.egg-info/requires.txt
9
+ src/FastSQLA.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ fastapi>=0.115.6
2
+ sqlalchemy[asyncio]>=2.0.37
3
+ structlog>=24.4.0
@@ -0,0 +1 @@
1
+ fastsqla
@@ -0,0 +1,196 @@
1
+ import math
2
+ import os
3
+ from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
4
+ from contextlib import asynccontextmanager
5
+ from typing import Annotated, Generic, TypeVar, TypedDict
6
+
7
+ from fastapi import Depends, Query
8
+ from pydantic import BaseModel, Field
9
+ from sqlalchemy import Result, Select, func, select
10
+ from sqlalchemy.ext.asyncio import (
11
+ AsyncEngine,
12
+ AsyncSession,
13
+ async_engine_from_config,
14
+ async_sessionmaker,
15
+ )
16
+ from sqlalchemy.ext.declarative import DeferredReflection
17
+ from sqlalchemy.orm import DeclarativeBase
18
+ from structlog import get_logger
19
+
20
+ __all__ = [
21
+ "Base",
22
+ "Collection",
23
+ "Item",
24
+ "Page",
25
+ "Paginate",
26
+ "PaginateType",
27
+ "Session",
28
+ "lifespan",
29
+ "new_pagination",
30
+ "open_session",
31
+ ]
32
+
33
+ SessionFactory = async_sessionmaker(expire_on_commit=False)
34
+
35
+ logger = get_logger(__name__)
36
+
37
+
38
+ class Base(DeclarativeBase, DeferredReflection):
39
+ __abstract__ = True
40
+
41
+
42
+ class State(TypedDict):
43
+ fastsqla_engine: AsyncEngine
44
+
45
+
46
+ @asynccontextmanager
47
+ async def lifespan(_) -> AsyncGenerator[State, None]:
48
+ prefix = "sqlalchemy_"
49
+ sqla_config = {k.lower(): v for k, v in os.environ.items()}
50
+ try:
51
+ engine = async_engine_from_config(sqla_config, prefix=prefix)
52
+
53
+ except KeyError as exc:
54
+ raise Exception(f"Missing {prefix}{exc.args[0]} in environ.") from exc
55
+
56
+ async with engine.begin() as conn:
57
+ await conn.run_sync(Base.prepare)
58
+
59
+ SessionFactory.configure(bind=engine)
60
+
61
+ await logger.ainfo("Configured SQLAlchemy.")
62
+
63
+ yield {"fastsqla_engine": engine}
64
+
65
+ SessionFactory.configure(bind=None)
66
+ await engine.dispose()
67
+
68
+ await logger.ainfo("Cleared SQLAlchemy config.")
69
+
70
+
71
+ @asynccontextmanager
72
+ async def open_session() -> AsyncGenerator[AsyncSession, None]:
73
+ session = SessionFactory()
74
+ try:
75
+ yield session
76
+
77
+ except Exception:
78
+ await logger.awarning("context failed: rolling back session.")
79
+ await session.rollback()
80
+ raise
81
+
82
+ else:
83
+ await logger.adebug("context succeeded: committing session.")
84
+ try:
85
+ await session.commit()
86
+
87
+ except Exception:
88
+ await logger.aexception("commit failed: rolling back session")
89
+ await session.rollback()
90
+ raise
91
+
92
+ finally:
93
+ await logger.adebug("closing session.")
94
+ await session.close()
95
+
96
+
97
+ async def new_session() -> AsyncGenerator[AsyncSession, None]:
98
+ async with open_session() as session:
99
+ yield session
100
+
101
+
102
+ Session = Annotated[AsyncSession, Depends(new_session)]
103
+
104
+
105
+ class Meta(BaseModel):
106
+ offset: int = Field(description="Current page offset.")
107
+ total_items: int = Field(description="Total number of items.")
108
+ total_pages: int = Field(description="Total number of pages.")
109
+ page_number: int = Field(description="Current page number. Starts at 1.")
110
+
111
+
112
+ T = TypeVar("T")
113
+
114
+
115
+ class Item(BaseModel, Generic[T]):
116
+ data: T
117
+
118
+
119
+ class Collection(BaseModel, Generic[T]):
120
+ data: list[T]
121
+
122
+
123
+ class Page(Collection[T]):
124
+ meta: Meta
125
+
126
+
127
+ async def _query_count(session: Session, stmt: Select) -> int:
128
+ result = await session.execute(select(func.count()).select_from(stmt.subquery()))
129
+ return result.scalar() # type: ignore
130
+
131
+
132
+ async def _paginate(
133
+ session: Session,
134
+ stmt: Select,
135
+ total_items: int,
136
+ offset: int,
137
+ limit: int,
138
+ result_processor: Callable[[Result], Iterable],
139
+ ):
140
+ total_pages = math.ceil(total_items / limit)
141
+ page_number = math.floor(offset / limit + 1)
142
+ result = await session.execute(stmt.offset(offset).limit(limit))
143
+ data = result_processor(result)
144
+ return Page(
145
+ data=data, # type:ignore
146
+ meta=Meta(
147
+ offset=offset,
148
+ total_items=total_items,
149
+ total_pages=total_pages,
150
+ page_number=page_number,
151
+ ),
152
+ )
153
+
154
+
155
+ def new_pagination(
156
+ min_page_size: int = 10,
157
+ max_page_size: int = 100,
158
+ query_count_dependency: Callable[..., Awaitable[int]] | None = None,
159
+ result_processor: Callable[[Result], Iterable] = lambda result: iter(
160
+ result.unique().scalars()
161
+ ),
162
+ ):
163
+ def default_dependency(
164
+ session: Session,
165
+ offset: int = Query(0, ge=0),
166
+ limit: int = Query(min_page_size, ge=1, le=max_page_size),
167
+ ) -> PaginateType[T]:
168
+ async def paginate(stmt: Select) -> Page:
169
+ total_items = await _query_count(session, stmt)
170
+ return await _paginate(
171
+ session, stmt, total_items, offset, limit, result_processor
172
+ )
173
+
174
+ return paginate
175
+
176
+ def dependency(
177
+ session: Session,
178
+ offset: int = Query(0, ge=0),
179
+ limit: int = Query(min_page_size, ge=1, le=max_page_size),
180
+ total_items: int = Depends(query_count_dependency),
181
+ ) -> PaginateType[T]:
182
+ async def paginate(stmt: Select) -> Page:
183
+ return await _paginate(
184
+ session, stmt, total_items, offset, limit, result_processor
185
+ )
186
+
187
+ return paginate
188
+
189
+ if query_count_dependency:
190
+ return dependency
191
+ else:
192
+ return default_dependency
193
+
194
+
195
+ type PaginateType[T] = Callable[[Select], Awaitable[Page[T]]]
196
+ Paginate = Annotated[PaginateType[T], Depends(new_pagination())]