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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastSQLA
3
- Version: 0.9.0
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&cursor=eyJ2IjoxLCJvcmRlciI6W1siaGVybyIsImlkIixmYWxzZSwiaW50Il1dLCJ2YWx1ZXMiOlsyXX0` 👇
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 `cursor` in the next query string. A `null` cursor means the
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&cursor=eyJ2IjoxLCJvcmRlciI6W1siaGVybyIsImlkIixmYWxzZSwiaW50Il1dLCJ2YWx1ZXMiOlsyXX0` 👇
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 `cursor` in the next query string. A `null` cursor means the
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.0"
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.0
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&cursor=eyJ2IjoxLCJvcmRlciI6W1siaGVybyIsImlkIixmYWxzZSwiaW50Il1dLCJ2YWx1ZXMiOlsyXX0` 👇
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 `cursor` in the next query string. A `null` cursor means the
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 BaseModel, ConfigDict, Field, TypeAdapter, ValidationError
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__ = ["Meta", "Page", "Paginate", "PaginateType", "new_pagination"]
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(BaseModel):
26
- next_cursor: str | None = Field(description="Next cursor, or null at the end.")
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
- type _Parameters = tuple[str | None, int | None]
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 = 100,
248
+ max_page_size: int = 1000,
216
249
  *,
217
- parameters_dependency: Callable[..., _Parameters | Awaitable[_Parameters]] | None = None,
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: Sync or async FastAPI dependency returning
229
- `(cursor, limit)`. A `None` limit uses `default_page_size`.
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
- cursor: str | None = Field(None, min_length=1)
249
- limit: int = Field(default_page_size, ge=1, le=max_page_size)
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
- ) -> _Parameters:
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[_Parameters, fastsqla.Depends(parameters_dependency)],
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
- cursor, limit = validated.cursor, validated.limit
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=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 cursor and limit query parameters."""
354
+ """Inject a forward paginator accepting next_cursor and limit query parameters."""
File without changes
File without changes