FastSQLA 0.2.4__py3-none-any.whl
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.dist-info/LICENSE +21 -0
- FastSQLA-0.2.4.dist-info/METADATA +261 -0
- FastSQLA-0.2.4.dist-info/RECORD +6 -0
- FastSQLA-0.2.4.dist-info/WHEEL +5 -0
- FastSQLA-0.2.4.dist-info/top_level.txt +1 -0
- fastsqla.py +196 -0
|
@@ -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
|
+
[](https://pypi.org/project/FastSQLA/)
|
|
47
|
+
[](https://conventionalcommits.org)
|
|
48
|
+
[](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,6 @@
|
|
|
1
|
+
fastsqla.py,sha256=FRN1RhVQPlD7hMNZG7wvSWZ7qdfXaqjmB3pXVa3z6Yw,5188
|
|
2
|
+
FastSQLA-0.2.4.dist-info/LICENSE,sha256=uNKcyfhTq0YUZxgSDiDGBHoJfflKjGWecSfWxpYe_O4,1070
|
|
3
|
+
FastSQLA-0.2.4.dist-info/METADATA,sha256=b0Lb3jCdUsQEEOLdW881z5nlLtfXNwaH3ir8PQRcSuw,7658
|
|
4
|
+
FastSQLA-0.2.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
5
|
+
FastSQLA-0.2.4.dist-info/top_level.txt,sha256=Uh-1ssTtuSS4_SYCBeDoDVOxqWTrRAPEBZkuih5isSE,9
|
|
6
|
+
FastSQLA-0.2.4.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastsqla
|
fastsqla.py
ADDED
|
@@ -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())]
|