auto-rest-api 0.1.2__tar.gz → 0.1.3__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.
Potentially problematic release.
This version of auto-rest-api might be problematic. Click here for more details.
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/PKG-INFO +1 -1
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/handlers.py +15 -7
- auto_rest_api-0.1.3/auto_rest/interfaces.py +115 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/queries.py +62 -1
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/pyproject.toml +6 -2
- auto_rest_api-0.1.2/auto_rest/interfaces.py +0 -126
- auto_rest_api-0.1.2/auto_rest/params.py +0 -174
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/LICENSE.md +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/README.md +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/__init__.py +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/__main__.py +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/app.py +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/cli.py +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/models.py +0 -0
- {auto_rest_api-0.1.2 → auto_rest_api-0.1.3}/auto_rest/routers.py +0 -0
|
@@ -51,16 +51,15 @@ This makes it easy to incorporate handlers into a FastAPI application.
|
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
53
|
import logging
|
|
54
|
-
from typing import Awaitable, Callable
|
|
54
|
+
from typing import Awaitable, Callable, Literal, Optional
|
|
55
55
|
|
|
56
|
-
from fastapi import Depends, Response
|
|
56
|
+
from fastapi import Depends, Query, Response
|
|
57
57
|
from pydantic import create_model
|
|
58
58
|
from pydantic.main import BaseModel as PydanticModel
|
|
59
59
|
from sqlalchemy import insert, MetaData, select, Table
|
|
60
60
|
|
|
61
61
|
from .interfaces import *
|
|
62
62
|
from .models import *
|
|
63
|
-
from .params import *
|
|
64
63
|
from .queries import *
|
|
65
64
|
|
|
66
65
|
__all__ = [
|
|
@@ -192,21 +191,30 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
|
|
|
192
191
|
"""
|
|
193
192
|
|
|
194
193
|
interface = create_interface(table)
|
|
194
|
+
interface_opt = create_interface(table, mode="optional")
|
|
195
|
+
columns = tuple(table.columns.keys())
|
|
195
196
|
|
|
196
197
|
async def list_records_handler(
|
|
197
198
|
response: Response,
|
|
198
199
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
_limit_: int = Query(0, ge=0, description="The maximum number of records to return."),
|
|
201
|
+
_offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
|
|
202
|
+
_order_by_: Optional[Literal[*columns]] = Query(None, description="The field name to sort by."),
|
|
203
|
+
_direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
|
|
201
204
|
) -> list[interface]:
|
|
202
205
|
"""Fetch a list of records from the database.
|
|
203
206
|
|
|
204
207
|
URL query parameters are used to enable filtering, ordering, and paginating returned values.
|
|
205
208
|
"""
|
|
206
209
|
|
|
210
|
+
response.headers["X-Pagination-Limit"] = str(_limit_)
|
|
211
|
+
response.headers["X-Pagination-Offset"] = str(_offset_)
|
|
212
|
+
response.headers["X-Order-By"] = str(_order_by_)
|
|
213
|
+
response.headers["X-Order-Direction"] = str(_direction_)
|
|
214
|
+
|
|
207
215
|
query = select(table)
|
|
208
|
-
query = apply_pagination_params(query,
|
|
209
|
-
query = apply_ordering_params(query,
|
|
216
|
+
query = apply_pagination_params(query, _limit_, _offset_)
|
|
217
|
+
query = apply_ordering_params(query, _order_by_, _direction_)
|
|
210
218
|
|
|
211
219
|
result = await execute_session_query(session, query)
|
|
212
220
|
return [row._mapping for row in result.all()]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Pydantic models are used to facilitate data validation and to define
|
|
2
|
+
interfaces for FastAPI endpoint handlers. The `interfaces` module
|
|
3
|
+
provides utility functions for converting SQLAlchemy models into
|
|
4
|
+
Pydantic interfaces. Interfaces can be created using different modes
|
|
5
|
+
which force interface fields to be optional or read only.
|
|
6
|
+
|
|
7
|
+
!!! example "Example: Creating an Interface"
|
|
8
|
+
|
|
9
|
+
The `create_interface_default` method creates an interface class
|
|
10
|
+
based on a SQLAlchemy table.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
default_interface = create_interface(database_model)
|
|
14
|
+
required_interface = create_interface(database_model, mode="required")
|
|
15
|
+
optional_interface = create_interface(database_model, mode="optional")
|
|
16
|
+
```
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Iterator, Literal
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel as PydanticModel, create_model
|
|
22
|
+
from sqlalchemy import Column, Table
|
|
23
|
+
|
|
24
|
+
__all__ = ["create_interface"]
|
|
25
|
+
|
|
26
|
+
MODE_TYPE = Literal["default", "required", "optional"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def iter_columns(table: Table, pk_only: bool = False) -> Iterator[Column]:
|
|
30
|
+
"""Iterate over the columns of a SQLAlchemy model.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
table: The table to iterate columns over.
|
|
34
|
+
pk_only: If True, only iterate over primary key columns.
|
|
35
|
+
|
|
36
|
+
Yields:
|
|
37
|
+
A column of the SQLAlchemy model.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
for column in table.columns.values():
|
|
41
|
+
if column.primary_key or not pk_only:
|
|
42
|
+
yield column
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_field_definition(col: Column, mode: MODE_TYPE = "default") -> tuple[type[any], any]:
|
|
46
|
+
"""Return a tuple with the type and default value for a database table column.
|
|
47
|
+
|
|
48
|
+
The returned tuple is compatible for use with Pydantic as a field definition
|
|
49
|
+
during dynamic model generation. The `mode` argument modifies returned
|
|
50
|
+
values to enforce different behavior in the generated Pydantic interface.
|
|
51
|
+
|
|
52
|
+
Modes:
|
|
53
|
+
default: Values are marked as (not)required based on the column schema.
|
|
54
|
+
required: Values are always marked required.
|
|
55
|
+
required: Values are always marked optional.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
col: The column to return values for.
|
|
59
|
+
mode: The mode to use when determining the default value.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The default value for the column.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
col_type = col.type.python_type
|
|
67
|
+
|
|
68
|
+
except NotImplementedError:
|
|
69
|
+
col_type = Any
|
|
70
|
+
|
|
71
|
+
col_default = getattr(col.default, "arg", col.default)
|
|
72
|
+
|
|
73
|
+
if mode == "required":
|
|
74
|
+
return col_type, ...
|
|
75
|
+
|
|
76
|
+
elif mode == "optional":
|
|
77
|
+
return col_type | None, col_default
|
|
78
|
+
|
|
79
|
+
elif mode == "default" and (col.nullable or col.default):
|
|
80
|
+
return col_type | None, col_default
|
|
81
|
+
|
|
82
|
+
elif mode == "default":
|
|
83
|
+
return col_type, ...
|
|
84
|
+
|
|
85
|
+
raise RuntimeError(f"Unknown mode: {mode}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_interface(table: Table, pk_only: bool = False, mode: MODE_TYPE = "default") -> type[PydanticModel]:
|
|
89
|
+
"""Create a Pydantic interface for a SQLAlchemy model where all fields are required.
|
|
90
|
+
|
|
91
|
+
Modes:
|
|
92
|
+
default: Values are marked as (not)required based on the column schema.
|
|
93
|
+
required: Values are always marked required.
|
|
94
|
+
required: Values are always marked optional.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
table: The SQLAlchemy table to create an interface for.
|
|
98
|
+
pk_only: If True, only include primary key columns.
|
|
99
|
+
mode: Whether to force fields to all be optional or required.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A dynamically generated Pydantic model with all fields required.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Map field names to the column type and default value.
|
|
106
|
+
fields = {
|
|
107
|
+
col.name: create_field_definition(col, mode) for col in iter_columns(table, pk_only)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Create a unique name for the interface
|
|
111
|
+
name = f"{table.name}-{mode.title()}"
|
|
112
|
+
if pk_only:
|
|
113
|
+
name += '-PK'
|
|
114
|
+
|
|
115
|
+
return create_model(name, __config__={'arbitrary_types_allowed': True}, **fields)
|
|
@@ -19,15 +19,18 @@ handling and provides a streamlined interface for database interactions.
|
|
|
19
19
|
result = await execute_session_query(async_session, query)
|
|
20
20
|
```
|
|
21
21
|
"""
|
|
22
|
+
from typing import Literal
|
|
22
23
|
|
|
23
24
|
from fastapi import HTTPException
|
|
24
|
-
from sqlalchemy import Executable, Result
|
|
25
|
+
from sqlalchemy import asc, desc, Executable, Result, Select
|
|
25
26
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
26
27
|
from starlette import status
|
|
27
28
|
|
|
28
29
|
from auto_rest.models import DBSession
|
|
29
30
|
|
|
30
31
|
__all__ = [
|
|
32
|
+
"apply_ordering_params",
|
|
33
|
+
"apply_pagination_params",
|
|
31
34
|
"commit_session",
|
|
32
35
|
"delete_session_record",
|
|
33
36
|
"execute_session_query",
|
|
@@ -35,6 +38,64 @@ __all__ = [
|
|
|
35
38
|
]
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
def apply_ordering_params(
|
|
42
|
+
query: Select,
|
|
43
|
+
order_by: str | None = None,
|
|
44
|
+
direction: Literal["desc", "asc"] = "asc"
|
|
45
|
+
) -> Select:
|
|
46
|
+
"""Apply ordering to a database query.
|
|
47
|
+
|
|
48
|
+
Returns a copy of the provided query with ordering parameters applied.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
query: The database query to apply parameters to.
|
|
52
|
+
order_by: The name of the column to order by.
|
|
53
|
+
direction: The direction to order by (defaults to "asc").
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A copy of the query modified to return ordered values.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
if order_by is None:
|
|
60
|
+
return query
|
|
61
|
+
|
|
62
|
+
if order_by not in query.columns:
|
|
63
|
+
raise ValueError(f"Invalid column name: {order_by}")
|
|
64
|
+
|
|
65
|
+
# Default to ascending order for an invalid ordering direction
|
|
66
|
+
if direction == "desc":
|
|
67
|
+
return query.order_by(desc(order_by))
|
|
68
|
+
|
|
69
|
+
elif direction == "asc":
|
|
70
|
+
return query.order_by(asc(order_by))
|
|
71
|
+
|
|
72
|
+
raise ValueError(f"Invalid direction, use 'asc' or 'desc': {direction}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def apply_pagination_params(query: Select, limit: int = 0, offset: int = 0) -> Select:
|
|
76
|
+
"""Apply pagination to a database query.
|
|
77
|
+
|
|
78
|
+
Returns a copy of the provided query with offset and limit parameters applied.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
query: The database query to apply parameters to.
|
|
82
|
+
limit: The number of results to return.
|
|
83
|
+
offset: The offset to start with.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A copy of the query modified to only return the paginated values.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
if offset < 0 or limit < 0:
|
|
90
|
+
raise ValueError("Pagination parameters cannot be negative")
|
|
91
|
+
|
|
92
|
+
# Do not apply pagination if not requested
|
|
93
|
+
if limit == 0:
|
|
94
|
+
return query
|
|
95
|
+
|
|
96
|
+
return query.offset(offset or 0).limit(limit)
|
|
97
|
+
|
|
98
|
+
|
|
38
99
|
async def commit_session(session: DBSession) -> None:
|
|
39
100
|
"""Commit a SQLAlchemy session.
|
|
40
101
|
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "auto-rest-api"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
description = "Automatically map database schemas and deploy per-table REST API endpoints."
|
|
10
10
|
authors = [{ name = "Better HPC LLC" }]
|
|
@@ -45,7 +45,6 @@ packages = [
|
|
|
45
45
|
{ include = "auto_rest" },
|
|
46
46
|
]
|
|
47
47
|
|
|
48
|
-
|
|
49
48
|
[tool.poetry.group.tests.dependencies]
|
|
50
49
|
coverage = "*"
|
|
51
50
|
|
|
@@ -56,3 +55,8 @@ mkdocstrings-python = "^1.13.0"
|
|
|
56
55
|
|
|
57
56
|
[tool.poetry.scripts]
|
|
58
57
|
auto-rest = "auto_rest.__main__:main"
|
|
58
|
+
|
|
59
|
+
[tool.coverage.run]
|
|
60
|
+
branch = true
|
|
61
|
+
source = ["auto_rest"]
|
|
62
|
+
omit = ["tests/*"]
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"""Pydantic models are used to facilitate data validation and to define
|
|
2
|
-
interfaces for FastAPI endpoint handlers. The `interfaces` module
|
|
3
|
-
provides utility functions for converting SQLAlchemy models into
|
|
4
|
-
Pydantic interfaces. Interfaces can be created using different modes
|
|
5
|
-
which force interface fields to be optional or read only.
|
|
6
|
-
|
|
7
|
-
!!! example "Example: Creating an Interface"
|
|
8
|
-
|
|
9
|
-
The `create_interface_default` method creates an interface class
|
|
10
|
-
based on a SQLAlchemy table.
|
|
11
|
-
|
|
12
|
-
```python
|
|
13
|
-
default_interface = create_interface_default(database_model)
|
|
14
|
-
required_interface = create_interface_required(database_model, mode="required")
|
|
15
|
-
optional_interface = create_interface_optional(database_model, mode="optional")
|
|
16
|
-
```
|
|
17
|
-
"""
|
|
18
|
-
from typing import Iterator, Literal
|
|
19
|
-
|
|
20
|
-
from pydantic import BaseModel as PydanticModel, create_model
|
|
21
|
-
from sqlalchemy import Column, Table
|
|
22
|
-
|
|
23
|
-
__all__ = ["create_interface"]
|
|
24
|
-
|
|
25
|
-
from sqlalchemy.sql.schema import ScalarElementColumnDefault
|
|
26
|
-
|
|
27
|
-
MODES = Literal["default", "required", "optional"]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def iter_columns(table: Table, pk_only: bool = False) -> Iterator[Column]:
|
|
31
|
-
"""Iterate over the columns of a SQLAlchemy model.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
table: The table to iterate columns over.
|
|
35
|
-
pk_only: If True, only iterate over primary key columns.
|
|
36
|
-
|
|
37
|
-
Yields:
|
|
38
|
-
A column of the SQLAlchemy model.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
for column in table.columns:
|
|
42
|
-
if column.primary_key or not pk_only:
|
|
43
|
-
yield column
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def get_column_type(col: Column) -> type[any]:
|
|
47
|
-
"""Return the Python type corresponding to a column's DB datatype.
|
|
48
|
-
|
|
49
|
-
Returns the `any` type for DBMS drivers that do not support mapping DB
|
|
50
|
-
types to Python primitives,
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
col: The column to determine a type for.
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
The equivalent Python type for the column data.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
return col.type.python_type
|
|
61
|
-
|
|
62
|
-
# Catch any error, but list the expected ones explicitly
|
|
63
|
-
except (NotImplementedError, Exception):
|
|
64
|
-
return any
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def get_column_default(col: Column, mode: MODES) -> any:
|
|
68
|
-
"""Return the default value for a column.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
col: The column to determine a default value for.
|
|
72
|
-
mode: The mode to use when determining the default value.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
The default value for the column.
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
# Extract the default value from the SQLAlchemy wrapper class
|
|
79
|
-
sqla_default = col.default
|
|
80
|
-
default = getattr(sqla_default, "arg", None) or sqla_default
|
|
81
|
-
|
|
82
|
-
if mode == "required":
|
|
83
|
-
return ...
|
|
84
|
-
|
|
85
|
-
elif mode == "optional":
|
|
86
|
-
return default
|
|
87
|
-
|
|
88
|
-
elif mode == "default":
|
|
89
|
-
if col.nullable or (col.default is not None):
|
|
90
|
-
return default
|
|
91
|
-
|
|
92
|
-
return ...
|
|
93
|
-
|
|
94
|
-
raise RuntimeError(f"Unknown mode: {mode}")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def create_interface(
|
|
98
|
-
table: Table,
|
|
99
|
-
pk_only: bool = False,
|
|
100
|
-
mode: MODES = "default"
|
|
101
|
-
) -> type[PydanticModel]:
|
|
102
|
-
"""Create a Pydantic interface for a SQLAlchemy model where all fields are required.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
table: The SQLAlchemy table to create an interface for.
|
|
106
|
-
pk_only: If True, only include primary key columns.
|
|
107
|
-
mode: Whether to force fields to all be optional or required.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
A dynamically generated Pydantic model with all fields required.
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
# Map field names to the column type and default value.
|
|
114
|
-
columns = iter_columns(table, pk_only)
|
|
115
|
-
fields = {
|
|
116
|
-
col.name: (get_column_type(col), get_column_default(col, mode))
|
|
117
|
-
for col in columns
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
# Dynamically create a unique name for the interface
|
|
121
|
-
name_parts = [table.name, mode.title()]
|
|
122
|
-
if pk_only:
|
|
123
|
-
name_parts.insert(1, 'PK')
|
|
124
|
-
|
|
125
|
-
interface_name = '-'.join(name_parts)
|
|
126
|
-
return create_model(interface_name, **fields)
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The `params` module provides utilities for extracting and applying query
|
|
3
|
-
parameters from incoming HTTP requests. These utilities ensure the consistent
|
|
4
|
-
parsing, validation, and application of query parameters, and automatically
|
|
5
|
-
update HTTP response headers to reflect applied query options.
|
|
6
|
-
|
|
7
|
-
Parameter functions are designed in pairs of two. The first function is a
|
|
8
|
-
factory for creating an injectable FastAPI dependency. The dependency
|
|
9
|
-
is used to parse parameters from incoming requests and applies high level
|
|
10
|
-
validation against the parsed values. The second function to applies the
|
|
11
|
-
validated arguments onto a SQLAlchemy query and returns the updated query.
|
|
12
|
-
|
|
13
|
-
!!! example "Example: Parameter Parsing and Application"
|
|
14
|
-
|
|
15
|
-
```python
|
|
16
|
-
from fastapi import FastAPI, Response
|
|
17
|
-
from sqlalchemy import select
|
|
18
|
-
from auto_rest.query_params import create_pagination_dependency, apply_pagination_params
|
|
19
|
-
|
|
20
|
-
app = FastAPI()
|
|
21
|
-
|
|
22
|
-
@app.get("/items/")
|
|
23
|
-
async def list_items(
|
|
24
|
-
pagination_params: dict = create_pagination_dependency(model),
|
|
25
|
-
response: Response
|
|
26
|
-
):
|
|
27
|
-
query = select(model)
|
|
28
|
-
query = apply_pagination_params(query, pagination_params, response)
|
|
29
|
-
return ... # Logic to further process and execute the query goes here
|
|
30
|
-
```
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
from collections.abc import Callable
|
|
34
|
-
from typing import Literal, Optional
|
|
35
|
-
|
|
36
|
-
from fastapi import Depends, Query
|
|
37
|
-
from sqlalchemy import asc, desc, Table
|
|
38
|
-
from sqlalchemy.sql.selectable import Select
|
|
39
|
-
from starlette.responses import Response
|
|
40
|
-
|
|
41
|
-
__all__ = [
|
|
42
|
-
"apply_ordering_params",
|
|
43
|
-
"apply_pagination_params",
|
|
44
|
-
"create_ordering_dependency",
|
|
45
|
-
"create_pagination_dependency",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def create_ordering_dependency(table: Table) -> Callable[..., dict]:
|
|
50
|
-
"""Create an injectable dependency for fetching ordering arguments from query parameters.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
table: The database table to create the dependency for.
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
An injectable FastAPI dependency.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
columns = tuple(table.columns.keys())
|
|
60
|
-
|
|
61
|
-
def get_ordering_params(
|
|
62
|
-
_order_by_: Optional[Literal[*columns]] = Query(None, description="The field name to sort by."),
|
|
63
|
-
_direction_: Optional[Literal["asc", "desc"]] = Query(None, description="Sort results in 'asc' or 'desc' order.")
|
|
64
|
-
) -> dict:
|
|
65
|
-
"""Extract ordering parameters from request query parameters.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
_order_by_: The field to order by.
|
|
69
|
-
_direction_: The direction to order by.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
dict: A dictionary containing the `order_by` and `direction` values.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
return {"order_by": _order_by_, "direction": _direction_}
|
|
76
|
-
|
|
77
|
-
return Depends(get_ordering_params)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def apply_ordering_params(query: Select, params: dict, response: Response) -> Select:
|
|
81
|
-
"""Apply ordering to a database query.
|
|
82
|
-
|
|
83
|
-
Returns a copy of the provided query with ordering parameters applied.
|
|
84
|
-
This method is compatible with parameters returned by the `get_ordering_params` method.
|
|
85
|
-
Ordering is not applied for invalid params, but response headers are still set.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
query: The database query to apply parameters to.
|
|
89
|
-
params: A dictionary containing parsed URL parameters.
|
|
90
|
-
response: The outgoing HTTP response object.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
A copy of the query modified to return ordered values.
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
order_by = params.get("order_by")
|
|
97
|
-
direction = params.get("direction")
|
|
98
|
-
|
|
99
|
-
# Set common response headers
|
|
100
|
-
response.headers["X-Order-By"] = str(order_by)
|
|
101
|
-
response.headers["X-Order-Direction"] = str(direction)
|
|
102
|
-
|
|
103
|
-
if order_by is None:
|
|
104
|
-
response.headers["X-Order-Applied"] = "false"
|
|
105
|
-
return query
|
|
106
|
-
|
|
107
|
-
# Default to ascending order for an invalid ordering direction
|
|
108
|
-
response.headers["X-Order-Applied"] = "true"
|
|
109
|
-
if direction == "desc":
|
|
110
|
-
return query.order_by(desc(order_by))
|
|
111
|
-
|
|
112
|
-
else:
|
|
113
|
-
return query.order_by(asc(order_by))
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def create_pagination_dependency(table: Table) -> Callable[..., dict]:
|
|
117
|
-
"""Create an injectable dependency for fetching pagination arguments from query parameters.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
table: The database table to create the dependency for.
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
An injectable FastAPI dependency.
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
def get_pagination_params(
|
|
127
|
-
_limit_: Optional[int] = Query(None, ge=0, description="The maximum number of records to return."),
|
|
128
|
-
_offset_: Optional[int] = Query(None, ge=0, description="The starting index of the returned records."),
|
|
129
|
-
) -> dict[str, int]:
|
|
130
|
-
"""Extract pagination parameters from request query parameters.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
_limit_: The maximum number of records to return.
|
|
134
|
-
_offset_: The starting index of the returned records.
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
dict: A dictionary containing the `limit` and `offset` values.
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
return {"limit": _limit_, "offset": _offset_}
|
|
141
|
-
|
|
142
|
-
return Depends(get_pagination_params)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def apply_pagination_params(query: Select, params: dict[str, int], response: Response) -> Select:
|
|
146
|
-
"""Apply pagination to a database query.
|
|
147
|
-
|
|
148
|
-
Returns a copy of the provided query with offset and limit parameters applied.
|
|
149
|
-
This method is compatible with parameters returned by the `get_pagination_params` method.
|
|
150
|
-
Pagination is not applied for invalid params, but response headers are still set.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
query: The database query to apply parameters to.
|
|
154
|
-
params: A dictionary containing parsed URL parameters.
|
|
155
|
-
response: The outgoing HTTP response object.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
A copy of the query modified to only return the paginated values.
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
limit = params.get("limit")
|
|
162
|
-
offset = params.get("offset")
|
|
163
|
-
|
|
164
|
-
# Set common response headers
|
|
165
|
-
response.headers["X-Pagination-Limit"] = str(limit)
|
|
166
|
-
response.headers["X-Pagination-Offset"] = str(offset)
|
|
167
|
-
|
|
168
|
-
# Do not apply pagination if not requested
|
|
169
|
-
if limit in (0, None):
|
|
170
|
-
response.headers["X-Pagination-Applied"] = "false"
|
|
171
|
-
return query
|
|
172
|
-
|
|
173
|
-
response.headers["X-Pagination-Applied"] = "true"
|
|
174
|
-
return query.offset(offset or 0).limit(limit)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|