auto-rest-api 0.1.1__py3-none-any.whl → 0.1.3__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.
Potentially problematic release.
This version of auto-rest-api might be problematic. Click here for more details.
- auto_rest/__main__.py +7 -5
- auto_rest/handlers.py +60 -48
- auto_rest/interfaces.py +115 -0
- auto_rest/models.py +6 -59
- auto_rest/queries.py +66 -5
- auto_rest/routers.py +94 -73
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.3.dist-info}/METADATA +1 -1
- auto_rest_api-0.1.3.dist-info/RECORD +14 -0
- auto_rest/params.py +0 -175
- auto_rest_api-0.1.1.dist-info/RECORD +0 -14
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.3.dist-info}/LICENSE.md +0 -0
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.3.dist-info}/WHEEL +0 -0
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.3.dist-info}/entry_points.txt +0 -0
auto_rest/__main__.py
CHANGED
|
@@ -69,13 +69,15 @@ def run_application(
|
|
|
69
69
|
app_version: version number for the generated OpenAPI schema.
|
|
70
70
|
"""
|
|
71
71
|
|
|
72
|
-
# Connect to and map the database.
|
|
73
72
|
logger.info(f"Mapping database schema for {db_name}.")
|
|
73
|
+
|
|
74
|
+
# Resolve database connection settings
|
|
74
75
|
db_url = create_db_url(driver=db_driver, host=db_host, port=db_port, database=db_name, username=db_user, password=db_pass)
|
|
75
76
|
db_kwargs = yaml.safe_load(db_config.read_text()) if db_config else {}
|
|
77
|
+
|
|
78
|
+
# Connect to and map the database.
|
|
76
79
|
db_conn = create_db_engine(db_url, **db_kwargs)
|
|
77
80
|
db_meta = create_db_metadata(db_conn)
|
|
78
|
-
db_models = create_db_models(db_meta)
|
|
79
81
|
|
|
80
82
|
# Build an empty application and dynamically add the requested functionality.
|
|
81
83
|
logger.info("Creating API application.")
|
|
@@ -83,9 +85,9 @@ def run_application(
|
|
|
83
85
|
app.include_router(create_welcome_router(), prefix="")
|
|
84
86
|
app.include_router(create_meta_router(db_conn, db_meta, app_title, app_version), prefix="/meta")
|
|
85
87
|
|
|
86
|
-
for
|
|
87
|
-
logger.info(f"Adding `/db/{
|
|
88
|
-
app.include_router(
|
|
88
|
+
for table_name, table in db_meta.tables.items():
|
|
89
|
+
logger.info(f"Adding `/db/{table_name}` endpoint.")
|
|
90
|
+
app.include_router(create_table_router(db_conn, table, enable_write), prefix=f"/db/{table_name}")
|
|
89
91
|
|
|
90
92
|
# Launch the API server.
|
|
91
93
|
logger.info(f"Launching API server on http://{server_host}:{server_port}.")
|
auto_rest/handlers.py
CHANGED
|
@@ -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
|
-
from sqlalchemy import insert, MetaData, select
|
|
60
|
-
from starlette.requests import Request
|
|
59
|
+
from sqlalchemy import insert, MetaData, select, Table
|
|
61
60
|
|
|
61
|
+
from .interfaces import *
|
|
62
62
|
from .models import *
|
|
63
|
-
from .params import *
|
|
64
63
|
from .queries import *
|
|
65
64
|
|
|
66
65
|
__all__ = [
|
|
@@ -180,120 +179,130 @@ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[Pydantic
|
|
|
180
179
|
return schema_handler
|
|
181
180
|
|
|
182
181
|
|
|
183
|
-
def create_list_records_handler(engine: DBEngine,
|
|
182
|
+
def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[list[PydanticModel]]]:
|
|
184
183
|
"""Create an endpoint handler that returns a list of records from a database table.
|
|
185
184
|
|
|
186
185
|
Args:
|
|
187
186
|
engine: Database engine to use when executing queries.
|
|
188
|
-
|
|
187
|
+
table: The database table to query against.
|
|
189
188
|
|
|
190
189
|
Returns:
|
|
191
190
|
An async function that returns a list of records from the given database model.
|
|
192
191
|
"""
|
|
193
192
|
|
|
194
|
-
interface =
|
|
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
|
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
|
|
215
|
+
query = select(table)
|
|
216
|
+
query = apply_pagination_params(query, _limit_, _offset_)
|
|
217
|
+
query = apply_ordering_params(query, _order_by_, _direction_)
|
|
218
|
+
|
|
210
219
|
result = await execute_session_query(session, query)
|
|
211
|
-
return [
|
|
220
|
+
return [row._mapping for row in result.all()]
|
|
212
221
|
|
|
213
222
|
return list_records_handler
|
|
214
223
|
|
|
215
224
|
|
|
216
|
-
def create_get_record_handler(engine: DBEngine,
|
|
225
|
+
def create_get_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
|
|
217
226
|
"""Create a function for handling GET requests against a single record in the database.
|
|
218
227
|
|
|
219
228
|
Args:
|
|
220
229
|
engine: Database engine to use when executing queries.
|
|
221
|
-
|
|
230
|
+
table: The database table to query against.
|
|
222
231
|
|
|
223
232
|
Returns:
|
|
224
|
-
An async function that returns a single record from the given database
|
|
233
|
+
An async function that returns a single record from the given database table.
|
|
225
234
|
"""
|
|
226
235
|
|
|
227
|
-
interface =
|
|
236
|
+
interface = create_interface(table)
|
|
237
|
+
pk_interface = create_interface(table, pk_only=True, mode='required')
|
|
228
238
|
|
|
229
239
|
async def get_record_handler(
|
|
230
|
-
|
|
240
|
+
pk: pk_interface = Depends(),
|
|
231
241
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
232
242
|
) -> interface:
|
|
233
243
|
"""Fetch a single record from the database."""
|
|
234
244
|
|
|
235
|
-
query = select(
|
|
245
|
+
query = select(table).filter_by(**pk.model_dump())
|
|
236
246
|
result = await execute_session_query(session, query)
|
|
237
247
|
record = get_record_or_404(result)
|
|
238
|
-
return
|
|
248
|
+
return record
|
|
239
249
|
|
|
240
250
|
return get_record_handler
|
|
241
251
|
|
|
242
252
|
|
|
243
|
-
def create_post_record_handler(engine: DBEngine,
|
|
253
|
+
def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
|
|
244
254
|
"""Create a function for handling POST requests against a record in the database.
|
|
245
255
|
|
|
246
256
|
Args:
|
|
247
257
|
engine: Database engine to use when executing queries.
|
|
248
|
-
|
|
258
|
+
table: The database table to query against.
|
|
249
259
|
|
|
250
260
|
Returns:
|
|
251
261
|
An async function that handles record creation.
|
|
252
262
|
"""
|
|
253
263
|
|
|
254
|
-
interface =
|
|
264
|
+
interface = create_interface(table)
|
|
255
265
|
|
|
256
266
|
async def post_record_handler(
|
|
257
267
|
data: interface,
|
|
258
268
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
259
|
-
) ->
|
|
269
|
+
) -> None:
|
|
260
270
|
"""Create a new record in the database."""
|
|
261
271
|
|
|
262
|
-
query = insert(
|
|
263
|
-
|
|
264
|
-
record = get_record_or_404(result)
|
|
265
|
-
|
|
272
|
+
query = insert(table).values(**data.dict())
|
|
273
|
+
await execute_session_query(session, query)
|
|
266
274
|
await commit_session(session)
|
|
267
|
-
return interface.model_validate(record.__dict__)
|
|
268
275
|
|
|
269
276
|
return post_record_handler
|
|
270
277
|
|
|
271
278
|
|
|
272
|
-
def create_put_record_handler(engine: DBEngine,
|
|
279
|
+
def create_put_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
|
|
273
280
|
"""Create a function for handling PUT requests against a record in the database.
|
|
274
281
|
|
|
275
282
|
Args:
|
|
276
283
|
engine: Database engine to use when executing queries.
|
|
277
|
-
|
|
284
|
+
table: The database table to query against.
|
|
278
285
|
|
|
279
286
|
Returns:
|
|
280
287
|
An async function that handles record updates.
|
|
281
288
|
"""
|
|
282
289
|
|
|
283
|
-
interface =
|
|
290
|
+
interface = create_interface(table)
|
|
291
|
+
opt_interface = create_interface(table, mode='optional')
|
|
292
|
+
pk_interface = create_interface(table, pk_only=True, mode='required')
|
|
284
293
|
|
|
285
294
|
async def put_record_handler(
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
data: opt_interface,
|
|
296
|
+
pk: pk_interface = Depends(),
|
|
288
297
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
289
298
|
) -> interface:
|
|
290
299
|
"""Replace record values in the database with the provided data."""
|
|
291
300
|
|
|
292
|
-
query = select(
|
|
301
|
+
query = select(table).filter_by(**pk.model_dump())
|
|
293
302
|
result = await execute_session_query(session, query)
|
|
294
303
|
record = get_record_or_404(result)
|
|
295
304
|
|
|
296
|
-
for key, value in data.
|
|
305
|
+
for key, value in data.model_dump().items():
|
|
297
306
|
setattr(record, key, value)
|
|
298
307
|
|
|
299
308
|
await commit_session(session)
|
|
@@ -302,57 +311,60 @@ def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[...,
|
|
|
302
311
|
return put_record_handler
|
|
303
312
|
|
|
304
313
|
|
|
305
|
-
def create_patch_record_handler(engine: DBEngine,
|
|
314
|
+
def create_patch_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
|
|
306
315
|
"""Create a function for handling PATCH requests against a record in the database.
|
|
307
316
|
|
|
308
317
|
Args:
|
|
309
318
|
engine: Database engine to use when executing queries.
|
|
310
|
-
|
|
319
|
+
table: The database table to query against.
|
|
311
320
|
|
|
312
321
|
Returns:
|
|
313
322
|
An async function that handles record updates.
|
|
314
323
|
"""
|
|
315
324
|
|
|
316
|
-
interface =
|
|
325
|
+
interface = create_interface(table)
|
|
326
|
+
pk_interface = create_interface(table, pk_only=True, mode='required')
|
|
317
327
|
|
|
318
328
|
async def patch_record_handler(
|
|
319
|
-
request: Request,
|
|
320
329
|
data: interface,
|
|
330
|
+
pk: pk_interface = Depends(),
|
|
321
331
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
322
332
|
) -> interface:
|
|
323
333
|
"""Update record values in the database with the provided data."""
|
|
324
334
|
|
|
325
|
-
query = select(
|
|
335
|
+
query = select(table).filter_by(**pk.model_dump())
|
|
326
336
|
result = await execute_session_query(session, query)
|
|
327
337
|
record = get_record_or_404(result)
|
|
328
338
|
|
|
329
|
-
for key, value in data.
|
|
339
|
+
for key, value in data.model_dump(exclude_unset=True).items():
|
|
330
340
|
setattr(record, key, value)
|
|
331
341
|
|
|
332
342
|
await commit_session(session)
|
|
333
|
-
return
|
|
343
|
+
return record
|
|
334
344
|
|
|
335
345
|
return patch_record_handler
|
|
336
346
|
|
|
337
347
|
|
|
338
|
-
def create_delete_record_handler(engine: DBEngine,
|
|
348
|
+
def create_delete_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[None]]:
|
|
339
349
|
"""Create a function for handling DELETE requests against a record in the database.
|
|
340
350
|
|
|
341
351
|
Args:
|
|
342
352
|
engine: Database engine to use when executing queries.
|
|
343
|
-
|
|
353
|
+
table: The database table to query against.
|
|
344
354
|
|
|
345
355
|
Returns:
|
|
346
356
|
An async function that handles record deletion.
|
|
347
357
|
"""
|
|
348
358
|
|
|
359
|
+
pk_interface = create_interface(table, pk_only=True, mode='required')
|
|
360
|
+
|
|
349
361
|
async def delete_record_handler(
|
|
350
|
-
|
|
362
|
+
pk: pk_interface = Depends(),
|
|
351
363
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
352
364
|
) -> None:
|
|
353
365
|
"""Delete a record from the database."""
|
|
354
366
|
|
|
355
|
-
query = select(
|
|
367
|
+
query = select(table).filter_by(**pk.model_dump())
|
|
356
368
|
result = await execute_session_query(session, query)
|
|
357
369
|
record = get_record_or_404(result)
|
|
358
370
|
|
auto_rest/interfaces.py
ADDED
|
@@ -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)
|
auto_rest/models.py
CHANGED
|
@@ -5,18 +5,16 @@ on the popular SQLAlchemy package, it natively supports multiple
|
|
|
5
5
|
Database Management Systems (DBMS) without requiring custom configuration
|
|
6
6
|
or setup.
|
|
7
7
|
|
|
8
|
-
!!! example "Example:
|
|
8
|
+
!!! example "Example: Mapping Database Metadata"
|
|
9
9
|
|
|
10
|
-
Utility functions are provided for connecting to the database
|
|
11
|
-
mapping the schema
|
|
12
|
-
the existing database structure.
|
|
10
|
+
Utility functions are provided for connecting to the database
|
|
11
|
+
and mapping the underlying schema.
|
|
13
12
|
|
|
14
13
|
```python
|
|
15
14
|
connection_args = dict(...)
|
|
16
15
|
db_url = create_db_url(**connection_args)
|
|
17
16
|
db_conn = create_db_engine(db_url)
|
|
18
17
|
db_meta = create_db_metadata(db_conn)
|
|
19
|
-
db_models = create_db_models(db_meta)
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
Support for asynchronous operations is automatically determined based on
|
|
@@ -36,29 +34,22 @@ import logging
|
|
|
36
34
|
from pathlib import Path
|
|
37
35
|
from typing import Callable
|
|
38
36
|
|
|
39
|
-
from pydantic.main import create_model, BaseModel as PydanticModel
|
|
40
37
|
from sqlalchemy import create_engine, Engine, MetaData, URL
|
|
41
38
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
|
42
|
-
from sqlalchemy.orm import
|
|
39
|
+
from sqlalchemy.orm import Session
|
|
43
40
|
|
|
44
41
|
__all__ = [
|
|
45
42
|
"DBEngine",
|
|
46
|
-
"DBModel",
|
|
47
43
|
"DBSession",
|
|
48
44
|
"create_db_engine",
|
|
49
|
-
"create_db_interface",
|
|
50
45
|
"create_db_metadata",
|
|
51
|
-
"create_db_models",
|
|
52
46
|
"create_db_url",
|
|
53
47
|
"create_session_iterator",
|
|
54
48
|
]
|
|
55
49
|
|
|
56
50
|
logger = logging.getLogger(__name__)
|
|
57
51
|
|
|
58
|
-
Base = declarative_base()
|
|
59
|
-
|
|
60
52
|
# Base classes and typing objects.
|
|
61
|
-
DBModel = type[Base]
|
|
62
53
|
DBEngine = Engine | AsyncEngine
|
|
63
54
|
DBSession = Session | AsyncSession
|
|
64
55
|
|
|
@@ -133,7 +124,7 @@ async def _async_reflect_metadata(engine: AsyncEngine, metadata: MetaData) -> No
|
|
|
133
124
|
"""Helper function used to reflect database metadata using an async engine."""
|
|
134
125
|
|
|
135
126
|
async with engine.connect() as connection:
|
|
136
|
-
await connection.run_sync(metadata.reflect)
|
|
127
|
+
await connection.run_sync(metadata.reflect, views=True)
|
|
137
128
|
|
|
138
129
|
|
|
139
130
|
def create_db_metadata(engine: DBEngine) -> MetaData:
|
|
@@ -153,55 +144,11 @@ def create_db_metadata(engine: DBEngine) -> MetaData:
|
|
|
153
144
|
asyncio.run(_async_reflect_metadata(engine, metadata))
|
|
154
145
|
|
|
155
146
|
else:
|
|
156
|
-
metadata.reflect(bind=engine)
|
|
147
|
+
metadata.reflect(bind=engine, views=True)
|
|
157
148
|
|
|
158
149
|
return metadata
|
|
159
150
|
|
|
160
151
|
|
|
161
|
-
def create_db_models(metadata: MetaData) -> dict[str, DBModel]:
|
|
162
|
-
"""Dynamically generate database models from a metadata instance.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
metadata: A reflection of database metadata.
|
|
166
|
-
|
|
167
|
-
Returns:
|
|
168
|
-
A dictionary mapping table names to database models.
|
|
169
|
-
"""
|
|
170
|
-
|
|
171
|
-
logger.debug("Building database models...")
|
|
172
|
-
models = {}
|
|
173
|
-
|
|
174
|
-
# Dynamically create a class for each table.
|
|
175
|
-
for table_name, table in metadata.tables.items():
|
|
176
|
-
logger.debug(f"> Creating model for table {table_name}.")
|
|
177
|
-
models[table_name] = type(
|
|
178
|
-
table_name.capitalize(),
|
|
179
|
-
(Base,),
|
|
180
|
-
{"__table__": table},
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
logger.debug(f"Successfully generated {len(models)} models.")
|
|
184
|
-
return models
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def create_db_interface(model: DBModel) -> type[PydanticModel]:
|
|
188
|
-
"""Create a Pydantic interface for a SQLAlchemy model.
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
model: A SQLAlchemy model to create an interface for.
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
A Pydantic model class with the same structure as the provided SQLAlchemy model.
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
fields = {
|
|
198
|
-
col.name: (col.type.python_type, col.default if col.default is not None else ...)
|
|
199
|
-
for col in model.__table__.columns
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return create_model(model.__name__, **fields)
|
|
203
|
-
|
|
204
|
-
|
|
205
152
|
def create_session_iterator(engine: DBEngine) -> Callable[[], DBSession]:
|
|
206
153
|
"""Create a generator for database sessions.
|
|
207
154
|
|
auto_rest/queries.py
CHANGED
|
@@ -10,7 +10,7 @@ handling and provides a streamlined interface for database interactions.
|
|
|
10
10
|
Query utilities seamlessly support synchronous and asynchronous session types.
|
|
11
11
|
|
|
12
12
|
```python
|
|
13
|
-
query = select(
|
|
13
|
+
query = select(SomeTable).where(SomeTable.id == item_id)
|
|
14
14
|
|
|
15
15
|
with Session(...) as sync_session:
|
|
16
16
|
result = await execute_session_query(sync_session, query)
|
|
@@ -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
|
|
|
@@ -101,7 +162,7 @@ def get_record_or_404(result: Result) -> any:
|
|
|
101
162
|
HTTPException: If the record is not found.
|
|
102
163
|
"""
|
|
103
164
|
|
|
104
|
-
if
|
|
105
|
-
|
|
165
|
+
if record := result.fetchone():
|
|
166
|
+
return record
|
|
106
167
|
|
|
107
|
-
|
|
168
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Record not found")
|
auto_rest/routers.py
CHANGED
|
@@ -24,15 +24,15 @@ routers to be added directly to an API application instance.
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
from fastapi import APIRouter
|
|
27
|
-
from sqlalchemy import MetaData
|
|
27
|
+
from sqlalchemy import MetaData, Table
|
|
28
28
|
from starlette import status
|
|
29
29
|
|
|
30
30
|
from auto_rest.handlers import *
|
|
31
|
-
from auto_rest.models import DBEngine
|
|
31
|
+
from auto_rest.models import DBEngine
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
"create_meta_router",
|
|
35
|
-
"
|
|
35
|
+
"create_table_router",
|
|
36
36
|
"create_welcome_router",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -45,7 +45,13 @@ def create_welcome_router() -> APIRouter:
|
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
47
|
router = APIRouter()
|
|
48
|
-
router.add_api_route(
|
|
48
|
+
router.add_api_route(
|
|
49
|
+
path="/",
|
|
50
|
+
methods=["GET"],
|
|
51
|
+
endpoint=create_welcome_handler(),
|
|
52
|
+
include_in_schema=False
|
|
53
|
+
)
|
|
54
|
+
|
|
49
55
|
return router
|
|
50
56
|
|
|
51
57
|
|
|
@@ -68,95 +74,110 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
|
|
|
68
74
|
router = APIRouter()
|
|
69
75
|
tags = ["Application Metadata"]
|
|
70
76
|
|
|
71
|
-
router.add_api_route(
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
router.add_api_route(
|
|
78
|
+
path="/app",
|
|
79
|
+
methods=["GET"],
|
|
80
|
+
endpoint=create_about_handler(name, version),
|
|
81
|
+
summary="Fetch application metadata.",
|
|
82
|
+
tags=tags
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
router.add_api_route(
|
|
86
|
+
path="/engine",
|
|
87
|
+
methods=["GET"],
|
|
88
|
+
endpoint=create_engine_handler(engine),
|
|
89
|
+
summary="Fetch database metadata.",
|
|
90
|
+
tags=tags
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
router.add_api_route(
|
|
94
|
+
path="/schema",
|
|
95
|
+
methods=["GET"],
|
|
96
|
+
endpoint=create_schema_handler(metadata),
|
|
97
|
+
summary="Fetch the database schema.",
|
|
98
|
+
tags=tags
|
|
99
|
+
)
|
|
100
|
+
|
|
74
101
|
return router
|
|
75
102
|
|
|
76
103
|
|
|
77
|
-
def
|
|
78
|
-
"""Create an API router with endpoint handlers for
|
|
104
|
+
def create_table_router(engine: DBEngine, table: Table, writeable: bool = False) -> APIRouter:
|
|
105
|
+
"""Create an API router with endpoint handlers for a given database table.
|
|
79
106
|
|
|
80
107
|
Args:
|
|
81
108
|
engine: The SQLAlchemy engine connected to the database.
|
|
82
|
-
|
|
109
|
+
table: The database table to create API endpoints for.
|
|
83
110
|
writeable: Whether the router should include support for write operations.
|
|
84
111
|
|
|
85
112
|
Returns:
|
|
86
|
-
An APIRouter instance with routes for database operations on the
|
|
113
|
+
An APIRouter instance with routes for database operations on the table.
|
|
87
114
|
"""
|
|
88
115
|
|
|
89
116
|
router = APIRouter()
|
|
90
117
|
|
|
91
118
|
# Construct path parameters from primary key columns
|
|
92
|
-
pk_columns = sorted(column.name for column in
|
|
119
|
+
pk_columns = sorted(column.name for column in table.primary_key.columns)
|
|
93
120
|
path_params_url = "/".join(f"{{{col_name}}}" for col_name in pk_columns)
|
|
94
|
-
path_params_openapi = {
|
|
95
|
-
"parameters": [
|
|
96
|
-
{"name": col_name, "in": "path", "required": True} for col_name in pk_columns
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
# Raise an error if no primary key columns are found
|
|
101
|
-
# (SQLAlchemy should ensure this never happens)
|
|
102
|
-
if not pk_columns: # pragma: no cover
|
|
103
|
-
raise RuntimeError(f"No primary key columns found for table {model.__tablename__}.")
|
|
104
|
-
|
|
105
|
-
# Define routes for read operations
|
|
106
|
-
router.add_api_route(
|
|
107
|
-
path="/",
|
|
108
|
-
methods=["GET"],
|
|
109
|
-
endpoint=create_list_records_handler(engine, model),
|
|
110
|
-
status_code=status.HTTP_200_OK,
|
|
111
|
-
tags=[model.__name__],
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
router.add_api_route(
|
|
115
|
-
path=f"/{path_params_url}/",
|
|
116
|
-
methods=["GET"],
|
|
117
|
-
endpoint=create_get_record_handler(engine, model),
|
|
118
|
-
status_code=status.HTTP_200_OK,
|
|
119
|
-
tags=[model.__name__],
|
|
120
|
-
openapi_extra=path_params_openapi
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
if not writeable:
|
|
124
|
-
return router
|
|
125
121
|
|
|
126
|
-
#
|
|
122
|
+
# Add route for read operations against the table
|
|
127
123
|
router.add_api_route(
|
|
128
124
|
path="/",
|
|
129
|
-
methods=["
|
|
130
|
-
endpoint=
|
|
131
|
-
status_code=status.HTTP_201_CREATED,
|
|
132
|
-
tags=[model.__name__],
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
router.add_api_route(
|
|
136
|
-
path=f"/{path_params_url}/",
|
|
137
|
-
methods=["PUT"],
|
|
138
|
-
endpoint=create_put_record_handler(engine, model),
|
|
139
|
-
status_code=status.HTTP_200_OK,
|
|
140
|
-
tags=[model.__name__],
|
|
141
|
-
openapi_extra=path_params_openapi
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
router.add_api_route(
|
|
145
|
-
path=f"/{path_params_url}/",
|
|
146
|
-
methods=["PATCH"],
|
|
147
|
-
endpoint=create_patch_record_handler(engine, model),
|
|
125
|
+
methods=["GET"],
|
|
126
|
+
endpoint=create_list_records_handler(engine, table),
|
|
148
127
|
status_code=status.HTTP_200_OK,
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
summary="Fetch multiple records from the table.",
|
|
129
|
+
tags=[table.name],
|
|
151
130
|
)
|
|
152
131
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
132
|
+
# Add route for write operations against the table
|
|
133
|
+
if writeable:
|
|
134
|
+
router.add_api_route(
|
|
135
|
+
path="/",
|
|
136
|
+
methods=["POST"],
|
|
137
|
+
endpoint=create_post_record_handler(engine, table),
|
|
138
|
+
status_code=status.HTTP_201_CREATED,
|
|
139
|
+
summary="Create a new record.",
|
|
140
|
+
tags=[table.name],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Add route for read operations against individual records
|
|
144
|
+
if pk_columns:
|
|
145
|
+
router.add_api_route(
|
|
146
|
+
path=f"/{path_params_url}/",
|
|
147
|
+
methods=["GET"],
|
|
148
|
+
endpoint=create_get_record_handler(engine, table),
|
|
149
|
+
status_code=status.HTTP_200_OK,
|
|
150
|
+
summary="Fetch a single record from the table.",
|
|
151
|
+
tags=[table.name],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Add routes for write operations against individual records
|
|
155
|
+
if pk_columns and writeable:
|
|
156
|
+
router.add_api_route(
|
|
157
|
+
path=f"/{path_params_url}/",
|
|
158
|
+
methods=["PUT"],
|
|
159
|
+
endpoint=create_put_record_handler(engine, table),
|
|
160
|
+
status_code=status.HTTP_200_OK,
|
|
161
|
+
summary="Replace a single record in the table.",
|
|
162
|
+
tags=[table.name],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
router.add_api_route(
|
|
166
|
+
path=f"/{path_params_url}/",
|
|
167
|
+
methods=["PATCH"],
|
|
168
|
+
endpoint=create_patch_record_handler(engine, table),
|
|
169
|
+
status_code=status.HTTP_200_OK,
|
|
170
|
+
summary="Update a single record in the table.",
|
|
171
|
+
tags=[table.name],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
router.add_api_route(
|
|
175
|
+
path=f"/{path_params_url}/",
|
|
176
|
+
methods=["DELETE"],
|
|
177
|
+
endpoint=create_delete_record_handler(engine, table),
|
|
178
|
+
status_code=status.HTTP_200_OK,
|
|
179
|
+
summary="Delete a single record from the table.",
|
|
180
|
+
tags=[table.name],
|
|
181
|
+
)
|
|
161
182
|
|
|
162
183
|
return router
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
|
|
2
|
+
auto_rest/__main__.py,sha256=lz6-LEpbu51Ah3QCzUzZhevjWhK5kmOxhuuqhX5t-io,3093
|
|
3
|
+
auto_rest/app.py,sha256=I6ZeHzKuhPTS1svLhMTvefjzTkMJgGpwVz1kdJ_9mtE,1887
|
|
4
|
+
auto_rest/cli.py,sha256=A7-kMTNNzqZB7jTUJBAVqfYm9RyYjENr0g_vK9JE0N4,5199
|
|
5
|
+
auto_rest/handlers.py,sha256=Z8DMv3KRYLBXlUKoE4e-2qE9YvG-IPgS57C0RpUDWv4,12603
|
|
6
|
+
auto_rest/interfaces.py,sha256=WB_0eMDjGF8DpnDN9INHqo7u4x3aklvzAYK4t3JwC7s,3779
|
|
7
|
+
auto_rest/models.py,sha256=HCJUQPBmkkfVFv9CRcPzMP66pQk_UIb3j3cdwHqodwE,5388
|
|
8
|
+
auto_rest/queries.py,sha256=Z2ATkcSYldV6BkcrmLogmBoDkPEkyFzFqI7qPcq86uc,4705
|
|
9
|
+
auto_rest/routers.py,sha256=RqBwLqVFU1OIFSCUuwOtTis8mT_zyWTaB_8SyIXjfe0,5727
|
|
10
|
+
auto_rest_api-0.1.3.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
|
|
11
|
+
auto_rest_api-0.1.3.dist-info/METADATA,sha256=Cs7u-2jZGTTlvBprPzdLVWl1uSAAjjLgeh5PuHo3tUk,2951
|
|
12
|
+
auto_rest_api-0.1.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
13
|
+
auto_rest_api-0.1.3.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
|
|
14
|
+
auto_rest_api-0.1.3.dist-info/RECORD,,
|
auto_rest/params.py
DELETED
|
@@ -1,175 +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
|
-
from collections.abc import Callable
|
|
33
|
-
from typing import Literal
|
|
34
|
-
|
|
35
|
-
from fastapi import Depends, Query
|
|
36
|
-
from sqlalchemy import asc, desc
|
|
37
|
-
from sqlalchemy.sql.selectable import Select
|
|
38
|
-
from starlette.responses import Response
|
|
39
|
-
|
|
40
|
-
from .models import DBModel
|
|
41
|
-
|
|
42
|
-
__all__ = [
|
|
43
|
-
"apply_ordering_params",
|
|
44
|
-
"apply_pagination_params",
|
|
45
|
-
"create_ordering_dependency",
|
|
46
|
-
"create_pagination_dependency",
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def create_ordering_dependency(model: type[DBModel]) -> Callable[..., dict]:
|
|
51
|
-
"""Create an injectable dependency for fetching ordering arguments from query parameters.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
model: The database model to create the dependency for.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
An injectable FastAPI dependency.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
columns = tuple(model.__table__.columns.keys())
|
|
61
|
-
|
|
62
|
-
def get_ordering_params(
|
|
63
|
-
_order_by_: Literal[*columns] = Query(None, description="The field name to sort by."),
|
|
64
|
-
_direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
|
|
65
|
-
) -> dict:
|
|
66
|
-
"""Extract ordering parameters from request query parameters.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
_order_by_: The field to order by.
|
|
70
|
-
_direction_: The direction to order by.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
dict: A dictionary containing the `order_by` and `direction` values.
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
return {"order_by": _order_by_, "direction": _direction_}
|
|
77
|
-
|
|
78
|
-
return Depends(get_ordering_params)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def apply_ordering_params(query: Select, params: dict, response: Response) -> Select:
|
|
82
|
-
"""Apply ordering to a database query.
|
|
83
|
-
|
|
84
|
-
Returns a copy of the provided query with ordering parameters applied.
|
|
85
|
-
This method is compatible with parameters returned by the `get_ordering_params` method.
|
|
86
|
-
Ordering is not applied for invalid params, but response headers are still set.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
query: The database query to apply parameters to.
|
|
90
|
-
params: A dictionary containing parsed URL parameters.
|
|
91
|
-
response: The outgoing HTTP response object.
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
A copy of the query modified to return ordered values.
|
|
95
|
-
"""
|
|
96
|
-
|
|
97
|
-
order_by = params.get("order_by")
|
|
98
|
-
direction = params.get("direction")
|
|
99
|
-
|
|
100
|
-
# Set common response headers
|
|
101
|
-
response.headers["X-Order-By"] = str(order_by)
|
|
102
|
-
response.headers["X-Order-Direction"] = str(direction)
|
|
103
|
-
|
|
104
|
-
if order_by is None:
|
|
105
|
-
response.headers["X-Order-Applied"] = "false"
|
|
106
|
-
return query
|
|
107
|
-
|
|
108
|
-
# Default to ascending order for an invalid ordering direction
|
|
109
|
-
response.headers["X-Order-Applied"] = "true"
|
|
110
|
-
if direction == "desc":
|
|
111
|
-
return query.order_by(desc(order_by))
|
|
112
|
-
|
|
113
|
-
else:
|
|
114
|
-
return query.order_by(asc(order_by))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def create_pagination_dependency(model: type[DBModel]) -> Callable[..., dict]:
|
|
118
|
-
"""Create an injectable dependency for fetching pagination arguments from query parameters.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
model: The database model to create the dependency for.
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
An injectable FastAPI dependency.
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
def get_pagination_params(
|
|
128
|
-
_limit_: int = Query(0, ge=0, description="The maximum number of records to return."),
|
|
129
|
-
_offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
|
|
130
|
-
) -> dict[str, int]:
|
|
131
|
-
"""Extract pagination parameters from request query parameters.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
_limit_: The maximum number of records to return.
|
|
135
|
-
_offset_: The starting index of the returned records.
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
dict: A dictionary containing the `limit` and `offset` values.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
return {"limit": _limit_, "offset": _offset_}
|
|
142
|
-
|
|
143
|
-
return Depends(get_pagination_params)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def apply_pagination_params(query: Select, params: dict[str, int], response: Response) -> Select:
|
|
147
|
-
"""Apply pagination to a database query.
|
|
148
|
-
|
|
149
|
-
Returns a copy of the provided query with offset and limit parameters applied.
|
|
150
|
-
This method is compatible with parameters returned by the `get_pagination_params` method.
|
|
151
|
-
Pagination is not applied for invalid params, but response headers are still set.
|
|
152
|
-
|
|
153
|
-
Args:
|
|
154
|
-
query: The database query to apply parameters to.
|
|
155
|
-
params: A dictionary containing parsed URL parameters.
|
|
156
|
-
response: The outgoing HTTP response object.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
A copy of the query modified to only return the paginated values.
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
limit = params.get("limit")
|
|
163
|
-
offset = params.get("offset")
|
|
164
|
-
|
|
165
|
-
# Set common response headers
|
|
166
|
-
response.headers["X-Pagination-Limit"] = str(limit)
|
|
167
|
-
response.headers["X-Pagination-Offset"] = str(offset)
|
|
168
|
-
|
|
169
|
-
# Do not apply pagination if not requested
|
|
170
|
-
if limit in (0, None):
|
|
171
|
-
response.headers["X-Pagination-Applied"] = "false"
|
|
172
|
-
return query
|
|
173
|
-
|
|
174
|
-
response.headers["X-Pagination-Applied"] = "true"
|
|
175
|
-
return query.offset(offset or 0).limit(limit)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
|
|
2
|
-
auto_rest/__main__.py,sha256=XvDFalpJBQ0QvZDr9f3tn108Skf80aYrnK8iohYLwBk,3085
|
|
3
|
-
auto_rest/app.py,sha256=I6ZeHzKuhPTS1svLhMTvefjzTkMJgGpwVz1kdJ_9mtE,1887
|
|
4
|
-
auto_rest/cli.py,sha256=A7-kMTNNzqZB7jTUJBAVqfYm9RyYjENr0g_vK9JE0N4,5199
|
|
5
|
-
auto_rest/handlers.py,sha256=QaqnYSxSBxB2YCffEf2uUlza6LplXBOPGoU2Yd8pe2k,11921
|
|
6
|
-
auto_rest/models.py,sha256=_SCDNO9811uJWndMG4EaqTPUh4PptcyfHJXrdQ4jhWc,6925
|
|
7
|
-
auto_rest/params.py,sha256=Qb3xS1t1rYDJD7C9095D4bZvZCKhH4za-lqwGKm2UrI,6067
|
|
8
|
-
auto_rest/queries.py,sha256=GzoCtxRPn9UJ7cnhYuPc8wZFppbqe7ykSlnesfWSr94,2926
|
|
9
|
-
auto_rest/routers.py,sha256=BN9B3Lzvme59a2a0DlLXYxosBCRjZtQsVxoljRggpyg,5326
|
|
10
|
-
auto_rest_api-0.1.1.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
|
|
11
|
-
auto_rest_api-0.1.1.dist-info/METADATA,sha256=snNqFpjnxbC2ntO5bhTETLtXAatgiiQYv9FL33n8G8s,2951
|
|
12
|
-
auto_rest_api-0.1.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
13
|
-
auto_rest_api-0.1.1.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
|
|
14
|
-
auto_rest_api-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|