FastSQLA 0.9.0__tar.gz → 0.9.2__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.9.0/src/FastSQLA.egg-info → fastsqla-0.9.2}/PKG-INFO +4 -4
- {fastsqla-0.9.0 → fastsqla-0.9.2}/README.md +3 -3
- {fastsqla-0.9.0 → fastsqla-0.9.2}/pyproject.toml +1 -1
- {fastsqla-0.9.0 → fastsqla-0.9.2/src/FastSQLA.egg-info}/PKG-INFO +4 -4
- {fastsqla-0.9.0 → fastsqla-0.9.2}/src/fastsqla/cursor.py +73 -25
- {fastsqla-0.9.0 → fastsqla-0.9.2}/LICENSE +0 -0
- {fastsqla-0.9.0 → fastsqla-0.9.2}/setup.cfg +0 -0
- {fastsqla-0.9.0 → fastsqla-0.9.2}/src/FastSQLA.egg-info/SOURCES.txt +0 -0
- {fastsqla-0.9.0 → fastsqla-0.9.2}/src/FastSQLA.egg-info/dependency_links.txt +0 -0
- {fastsqla-0.9.0 → fastsqla-0.9.2}/src/FastSQLA.egg-info/requires.txt +0 -0
- {fastsqla-0.9.0 → fastsqla-0.9.2}/src/FastSQLA.egg-info/top_level.txt +0 -0
- {fastsqla-0.9.0 → fastsqla-0.9.2}/src/fastsqla/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastSQLA
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination.
|
|
5
5
|
Author-email: Hadrien David <h@driendavid.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -163,7 +163,7 @@ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/
|
|
|
163
163
|
|
|
164
164
|
<center>
|
|
165
165
|
|
|
166
|
-
👇 `/heros?limit=2&
|
|
166
|
+
👇 `/heros?limit=2&next_cursor=eyJ2IjoxLCJvcmRlciI6W1siaGVybyIsImlkIixmYWxzZSwiaW50Il1dLCJ2YWx1ZXMiOlsyXX0` 👇
|
|
167
167
|
|
|
168
168
|
</center>
|
|
169
169
|
|
|
@@ -187,8 +187,8 @@ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/
|
|
|
187
187
|
}
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
Pass `meta.next_cursor` as `
|
|
191
|
-
end of the results.
|
|
190
|
+
Pass `meta.next_cursor` as `next_cursor` in the next query string. A `null` cursor
|
|
191
|
+
means the end of the results.
|
|
192
192
|
|
|
193
193
|
* Pagination customization:
|
|
194
194
|
|
|
@@ -116,7 +116,7 @@ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/
|
|
|
116
116
|
|
|
117
117
|
<center>
|
|
118
118
|
|
|
119
|
-
👇 `/heros?limit=2&
|
|
119
|
+
👇 `/heros?limit=2&next_cursor=eyJ2IjoxLCJvcmRlciI6W1siaGVybyIsImlkIixmYWxzZSwiaW50Il1dLCJ2YWx1ZXMiOlsyXX0` 👇
|
|
120
120
|
|
|
121
121
|
</center>
|
|
122
122
|
|
|
@@ -140,8 +140,8 @@ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/
|
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
Pass `meta.next_cursor` as `
|
|
144
|
-
end of the results.
|
|
143
|
+
Pass `meta.next_cursor` as `next_cursor` in the next query string. A `null` cursor
|
|
144
|
+
means the end of the results.
|
|
145
145
|
|
|
146
146
|
* Pagination customization:
|
|
147
147
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FastSQLA"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
8
8
|
description = "SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastSQLA
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: SQLAlchemy extension for FastAPI that supports asynchronous sessions and includes built-in pagination.
|
|
5
5
|
Author-email: Hadrien David <h@driendavid.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -163,7 +163,7 @@ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/
|
|
|
163
163
|
|
|
164
164
|
<center>
|
|
165
165
|
|
|
166
|
-
👇 `/heros?limit=2&
|
|
166
|
+
👇 `/heros?limit=2&next_cursor=eyJ2IjoxLCJvcmRlciI6W1siaGVybyIsImlkIixmYWxzZSwiaW50Il1dLCJ2YWx1ZXMiOlsyXX0` 👇
|
|
167
167
|
|
|
168
168
|
</center>
|
|
169
169
|
|
|
@@ -187,8 +187,8 @@ following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/
|
|
|
187
187
|
}
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
Pass `meta.next_cursor` as `
|
|
191
|
-
end of the results.
|
|
190
|
+
Pass `meta.next_cursor` as `next_cursor` in the next query string. A `null` cursor
|
|
191
|
+
means the end of the results.
|
|
192
192
|
|
|
193
193
|
* Pagination customization:
|
|
194
194
|
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import inspect
|
|
2
3
|
import json
|
|
3
4
|
from collections.abc import Awaitable, Callable
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from datetime import date, datetime
|
|
6
7
|
from decimal import Decimal
|
|
7
|
-
from typing import Annotated, Any
|
|
8
|
+
from typing import Annotated, Any, TypedDict
|
|
8
9
|
from uuid import UUID
|
|
9
10
|
|
|
10
11
|
import sqlalchemy as sa
|
|
11
12
|
from fastapi import HTTPException, Query
|
|
12
13
|
from fastapi.exceptions import RequestValidationError
|
|
13
|
-
from pydantic import
|
|
14
|
+
from pydantic import (
|
|
15
|
+
BaseModel,
|
|
16
|
+
ConfigDict,
|
|
17
|
+
Field,
|
|
18
|
+
RootModel,
|
|
19
|
+
TypeAdapter,
|
|
20
|
+
ValidationError,
|
|
21
|
+
field_validator,
|
|
22
|
+
)
|
|
14
23
|
from sqlalchemy import Select
|
|
15
24
|
from sqlalchemy.dialects import mysql
|
|
16
25
|
from sqlalchemy.sql import operators, visitors
|
|
@@ -19,11 +28,25 @@ from sqlalchemy.sql.selectable import Join
|
|
|
19
28
|
|
|
20
29
|
import fastsqla
|
|
21
30
|
|
|
22
|
-
__all__ = [
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Meta",
|
|
33
|
+
"Page",
|
|
34
|
+
"Paginate",
|
|
35
|
+
"PaginateType",
|
|
36
|
+
"PaginationParameters",
|
|
37
|
+
"ParametersDependency",
|
|
38
|
+
"new_pagination",
|
|
39
|
+
]
|
|
23
40
|
|
|
24
41
|
|
|
25
|
-
class Meta(
|
|
26
|
-
|
|
42
|
+
class Meta(RootModel[dict[str, str | None]]):
|
|
43
|
+
root: dict[str, str | None] = Field(
|
|
44
|
+
min_length=1, max_length=1, description="One named cursor, or null at the end."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def next_cursor(self) -> str | None:
|
|
49
|
+
return next(iter(self.root.values()))
|
|
27
50
|
|
|
28
51
|
|
|
29
52
|
class Page[T](fastsqla.Collection[T]):
|
|
@@ -33,7 +56,17 @@ class Page[T](fastsqla.Collection[T]):
|
|
|
33
56
|
|
|
34
57
|
|
|
35
58
|
type PaginateType[T] = Callable[[Select], Awaitable[Page[T]]]
|
|
36
|
-
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PaginationParameters(TypedDict):
|
|
62
|
+
cursor_name: str
|
|
63
|
+
cursor: str | None
|
|
64
|
+
limit: int | None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
type ParametersDependency = Callable[..., Awaitable[PaginationParameters]]
|
|
68
|
+
|
|
69
|
+
|
|
37
70
|
type _Value = int | str | UUID | datetime | date | Decimal
|
|
38
71
|
|
|
39
72
|
|
|
@@ -212,9 +245,9 @@ def _condition(
|
|
|
212
245
|
|
|
213
246
|
def new_pagination[T](
|
|
214
247
|
default_page_size: int = 10,
|
|
215
|
-
max_page_size: int =
|
|
248
|
+
max_page_size: int = 1000,
|
|
216
249
|
*,
|
|
217
|
-
parameters_dependency:
|
|
250
|
+
parameters_dependency: ParametersDependency | None = None,
|
|
218
251
|
row_mapper: Callable[[sa.Row], T] = lambda row: row[0],
|
|
219
252
|
) -> Any:
|
|
220
253
|
"""Create a generic pagination dependency: `Paginate = new_pagination(...)`.
|
|
@@ -225,14 +258,15 @@ def new_pagination[T](
|
|
|
225
258
|
Args:
|
|
226
259
|
default_page_size: Default limit when the client omits it.
|
|
227
260
|
max_page_size: Maximum accepted limit.
|
|
228
|
-
parameters_dependency:
|
|
229
|
-
|
|
261
|
+
parameters_dependency: Async FastAPI dependency returning a cursor name,
|
|
262
|
+
cursor value, and limit. A `None` limit uses `default_page_size`.
|
|
230
263
|
row_mapper: Maps each original result row to exactly one response item.
|
|
231
264
|
|
|
232
265
|
Returns:
|
|
233
266
|
Generic annotated dependency with a one-row-to-one-item mapper.
|
|
234
267
|
|
|
235
268
|
Raises:
|
|
269
|
+
TypeError: The parameter dependency is not async.
|
|
236
270
|
ValueError: Page-size bounds or the supplied Select are unsupported.
|
|
237
271
|
"""
|
|
238
272
|
if (
|
|
@@ -241,36 +275,50 @@ def new_pagination[T](
|
|
|
241
275
|
or not 1 <= default_page_size <= max_page_size
|
|
242
276
|
):
|
|
243
277
|
raise ValueError("Require 1 <= default_page_size <= max_page_size")
|
|
244
|
-
|
|
278
|
+
if parameters_dependency is not None and not (
|
|
279
|
+
callable(parameters_dependency)
|
|
280
|
+
and (
|
|
281
|
+
inspect.iscoroutinefunction(parameters_dependency)
|
|
282
|
+
or inspect.iscoroutinefunction(type(parameters_dependency).__call__)
|
|
283
|
+
)
|
|
284
|
+
):
|
|
285
|
+
raise TypeError("parameters_dependency must be async")
|
|
245
286
|
class Parameters(BaseModel):
|
|
246
|
-
model_config = ConfigDict(strict=True)
|
|
287
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
288
|
+
|
|
289
|
+
cursor_name: str = Field(min_length=1)
|
|
290
|
+
cursor: str | None = Field(min_length=1)
|
|
291
|
+
limit: int | None = Field(ge=1, le=max_page_size)
|
|
247
292
|
|
|
248
|
-
|
|
249
|
-
|
|
293
|
+
@field_validator("cursor_name")
|
|
294
|
+
@classmethod
|
|
295
|
+
def valid_cursor_name(cls, name: str) -> str:
|
|
296
|
+
if name != name.strip():
|
|
297
|
+
raise ValueError("Cursor name cannot have surrounding whitespace")
|
|
298
|
+
return name
|
|
250
299
|
|
|
251
300
|
async def query_parameters(
|
|
252
|
-
cursor: str | None = Query(None, min_length=1),
|
|
301
|
+
cursor: str | None = Query(None, min_length=1, alias="next_cursor"),
|
|
253
302
|
limit: int = Query(default_page_size, ge=1, le=max_page_size),
|
|
254
|
-
) ->
|
|
255
|
-
return cursor, limit
|
|
303
|
+
) -> PaginationParameters:
|
|
304
|
+
return {"cursor_name": "next_cursor", "cursor": cursor, "limit": limit}
|
|
256
305
|
|
|
257
306
|
if parameters_dependency is None:
|
|
258
307
|
parameters_dependency = query_parameters
|
|
259
308
|
|
|
260
309
|
async def dependency(
|
|
261
310
|
session: fastsqla.Session,
|
|
262
|
-
parameters: Annotated[
|
|
311
|
+
parameters: Annotated[PaginationParameters, fastsqla.Depends(parameters_dependency)],
|
|
263
312
|
) -> PaginateType[T]:
|
|
264
|
-
cursor, limit = parameters
|
|
265
313
|
try:
|
|
266
|
-
validated = Parameters(
|
|
267
|
-
cursor=cursor, limit=default_page_size if limit is None else limit
|
|
268
|
-
)
|
|
314
|
+
validated = Parameters.model_validate(parameters)
|
|
269
315
|
except ValidationError as error:
|
|
270
316
|
raise RequestValidationError(
|
|
271
317
|
error.errors(include_url=False, include_input=False)
|
|
272
318
|
) from error
|
|
273
|
-
|
|
319
|
+
cursor_name = validated.cursor_name
|
|
320
|
+
cursor = validated.cursor
|
|
321
|
+
limit = validated.limit if validated.limit is not None else default_page_size
|
|
274
322
|
|
|
275
323
|
async def paginate(stmt: Select) -> Page[T]:
|
|
276
324
|
order = _order(stmt)
|
|
@@ -295,7 +343,7 @@ def new_pagination[T](
|
|
|
295
343
|
next_cursor = _encode(order, cursor_values)
|
|
296
344
|
original = frozen().columns(*range(width)).all()
|
|
297
345
|
data = [row_mapper(row) for row in original[:limit]]
|
|
298
|
-
return Page(data=data, meta=Meta(next_cursor
|
|
346
|
+
return Page(data=data, meta=Meta({cursor_name: next_cursor}))
|
|
299
347
|
|
|
300
348
|
return paginate
|
|
301
349
|
|
|
@@ -303,4 +351,4 @@ def new_pagination[T](
|
|
|
303
351
|
|
|
304
352
|
|
|
305
353
|
Paginate = new_pagination()
|
|
306
|
-
"""Inject a forward paginator accepting
|
|
354
|
+
"""Inject a forward paginator accepting next_cursor and limit query parameters."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|