auto-rest-api 0.1.0__tar.gz → 0.1.1__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.0 → auto_rest_api-0.1.1}/PKG-INFO +2 -2
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/__main__.py +3 -4
- auto_rest_api-0.1.1/auto_rest/app.py +67 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/handlers.py +13 -13
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/models.py +6 -4
- auto_rest_api-0.1.1/auto_rest/params.py +175 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/routers.py +4 -10
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/pyproject.toml +2 -2
- auto_rest_api-0.1.0/auto_rest/params.py +0 -147
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/LICENSE.md +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/README.md +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/__init__.py +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/cli.py +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.1}/auto_rest/queries.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: auto-rest-api
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Automatically map database schemas and deploy per-table REST API endpoints.
|
|
5
5
|
License: GPL-3.0-only
|
|
6
6
|
Keywords: Better,HPC,automatic,rest,api
|
|
7
7
|
Author: Better HPC LLC
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Classifier: Environment :: Web Environment
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
import uvicorn
|
|
7
6
|
import yaml
|
|
8
|
-
from fastapi import FastAPI
|
|
9
7
|
|
|
8
|
+
from .app import *
|
|
10
9
|
from .cli import *
|
|
11
10
|
from .models import *
|
|
12
11
|
from .routers import *
|
|
@@ -80,7 +79,7 @@ def run_application(
|
|
|
80
79
|
|
|
81
80
|
# Build an empty application and dynamically add the requested functionality.
|
|
82
81
|
logger.info("Creating API application.")
|
|
83
|
-
app =
|
|
82
|
+
app = create_app(app_title, app_version, enable_docs)
|
|
84
83
|
app.include_router(create_welcome_router(), prefix="")
|
|
85
84
|
app.include_router(create_meta_router(db_conn, db_meta, app_title, app_version), prefix="/meta")
|
|
86
85
|
|
|
@@ -90,4 +89,4 @@ def run_application(
|
|
|
90
89
|
|
|
91
90
|
# Launch the API server.
|
|
92
91
|
logger.info(f"Launching API server on http://{server_host}:{server_port}.")
|
|
93
|
-
|
|
92
|
+
run_server(app, server_host, server_port)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `app` module provides factory functions and utilities for building and
|
|
3
|
+
deploying Fast-API applications.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
!!! example "Example: Build and Deploy an API"
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from auto_rest.app import create_app, run_server
|
|
10
|
+
|
|
11
|
+
app = create_app(app_title="My Application", app_version="1.2.3", enable_docs=True)
|
|
12
|
+
... # Add endpoints to the application here
|
|
13
|
+
run_server(app, host="127.0.0.1", port=8081)
|
|
14
|
+
```
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import uvicorn
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
+
|
|
21
|
+
__all__ = ["create_app", "run_server"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
|
|
25
|
+
"""Create and configure a FastAPI application instance.
|
|
26
|
+
|
|
27
|
+
This function initializes a FastAPI app with a customizable title, version,
|
|
28
|
+
and optional documentation routes. It also configures application middleware
|
|
29
|
+
for CORS policies.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
app_title: The title of the FastAPI application.
|
|
33
|
+
app_version: The version of the FastAPI application.
|
|
34
|
+
enable_docs: Whether to enable the `/docs/` endpoint.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
FastAPI: A configured FastAPI application instance.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
app = FastAPI(
|
|
41
|
+
title=app_title,
|
|
42
|
+
version=app_version,
|
|
43
|
+
docs_url="/docs/" if enable_docs else None,
|
|
44
|
+
redoc_url=None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
app.add_middleware(
|
|
48
|
+
CORSMiddleware,
|
|
49
|
+
allow_origins=["*"],
|
|
50
|
+
allow_credentials=True,
|
|
51
|
+
allow_methods=["*"],
|
|
52
|
+
allow_headers=["*"],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return app
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
|
|
59
|
+
"""Deploy a FastAPI application server.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
app: The FastAPI application to run.
|
|
63
|
+
host: The hostname or IP address for the server to bind to.
|
|
64
|
+
port: The port number for the server to listen on.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
uvicorn.run(app, host=host, port=port, log_level="error")
|
|
@@ -55,7 +55,7 @@ from typing import Awaitable, Callable
|
|
|
55
55
|
|
|
56
56
|
from fastapi import Depends, Response
|
|
57
57
|
from pydantic import create_model
|
|
58
|
-
from pydantic.main import
|
|
58
|
+
from pydantic.main import BaseModel as PydanticModel
|
|
59
59
|
from sqlalchemy import insert, MetaData, select
|
|
60
60
|
from starlette.requests import Request
|
|
61
61
|
|
|
@@ -79,7 +79,7 @@ __all__ = [
|
|
|
79
79
|
logger = logging.getLogger(__name__)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def create_welcome_handler() -> Callable[[], Awaitable[
|
|
82
|
+
def create_welcome_handler() -> Callable[[], Awaitable[PydanticModel]]:
|
|
83
83
|
"""Create an endpoint handler that returns an application welcome message.
|
|
84
84
|
|
|
85
85
|
Returns:
|
|
@@ -96,7 +96,7 @@ def create_welcome_handler() -> Callable[[], Awaitable[ModelT]]:
|
|
|
96
96
|
return welcome_handler
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[
|
|
99
|
+
def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[PydanticModel]]:
|
|
100
100
|
"""Create an endpoint handler that returns the application name and version number.
|
|
101
101
|
|
|
102
102
|
Args:
|
|
@@ -104,7 +104,7 @@ def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[Mode
|
|
|
104
104
|
version: The returned version identifier.
|
|
105
105
|
|
|
106
106
|
Returns:
|
|
107
|
-
An async function that returns
|
|
107
|
+
An async function that returns application info.
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
110
|
interface = create_model("Version", version=(str, version), name=(str, name))
|
|
@@ -117,7 +117,7 @@ def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[Mode
|
|
|
117
117
|
return about_handler
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[
|
|
120
|
+
def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[PydanticModel]]:
|
|
121
121
|
"""Create an endpoint handler that returns configuration details for a database engine.
|
|
122
122
|
|
|
123
123
|
Args:
|
|
@@ -141,7 +141,7 @@ def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[ModelT]]:
|
|
|
141
141
|
return meta_handler
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[
|
|
144
|
+
def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[PydanticModel]]:
|
|
145
145
|
"""Create an endpoint handler that returns the database schema.
|
|
146
146
|
|
|
147
147
|
Args:
|
|
@@ -180,7 +180,7 @@ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[ModelT]]
|
|
|
180
180
|
return schema_handler
|
|
181
181
|
|
|
182
182
|
|
|
183
|
-
def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[list[
|
|
183
|
+
def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[list[PydanticModel]]]:
|
|
184
184
|
"""Create an endpoint handler that returns a list of records from a database table.
|
|
185
185
|
|
|
186
186
|
Args:
|
|
@@ -196,8 +196,8 @@ def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..
|
|
|
196
196
|
async def list_records_handler(
|
|
197
197
|
response: Response,
|
|
198
198
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
199
|
-
pagination_params: dict[str, int] =
|
|
200
|
-
ordering_params: dict[str, int] =
|
|
199
|
+
pagination_params: dict[str, int] = create_pagination_dependency(model),
|
|
200
|
+
ordering_params: dict[str, int] = create_ordering_dependency(model),
|
|
201
201
|
) -> list[interface]:
|
|
202
202
|
"""Fetch a list of records from the database.
|
|
203
203
|
|
|
@@ -213,7 +213,7 @@ def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..
|
|
|
213
213
|
return list_records_handler
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
def create_get_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[
|
|
216
|
+
def create_get_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
|
|
217
217
|
"""Create a function for handling GET requests against a single record in the database.
|
|
218
218
|
|
|
219
219
|
Args:
|
|
@@ -240,7 +240,7 @@ def create_get_record_handler(engine: DBEngine, model: DBModel) -> Callable[...,
|
|
|
240
240
|
return get_record_handler
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
def create_post_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[
|
|
243
|
+
def create_post_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
|
|
244
244
|
"""Create a function for handling POST requests against a record in the database.
|
|
245
245
|
|
|
246
246
|
Args:
|
|
@@ -269,7 +269,7 @@ def create_post_record_handler(engine: DBEngine, model: DBModel) -> Callable[...
|
|
|
269
269
|
return post_record_handler
|
|
270
270
|
|
|
271
271
|
|
|
272
|
-
def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[
|
|
272
|
+
def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
|
|
273
273
|
"""Create a function for handling PUT requests against a record in the database.
|
|
274
274
|
|
|
275
275
|
Args:
|
|
@@ -302,7 +302,7 @@ def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[...,
|
|
|
302
302
|
return put_record_handler
|
|
303
303
|
|
|
304
304
|
|
|
305
|
-
def create_patch_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[
|
|
305
|
+
def create_patch_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
|
|
306
306
|
"""Create a function for handling PATCH requests against a record in the database.
|
|
307
307
|
|
|
308
308
|
Args:
|
|
@@ -36,7 +36,7 @@ import logging
|
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
from typing import Callable
|
|
38
38
|
|
|
39
|
-
from pydantic.main import create_model,
|
|
39
|
+
from pydantic.main import create_model, BaseModel as PydanticModel
|
|
40
40
|
from sqlalchemy import create_engine, Engine, MetaData, URL
|
|
41
41
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
|
42
42
|
from sqlalchemy.orm import declarative_base, Session
|
|
@@ -55,8 +55,10 @@ __all__ = [
|
|
|
55
55
|
|
|
56
56
|
logger = logging.getLogger(__name__)
|
|
57
57
|
|
|
58
|
+
Base = declarative_base()
|
|
59
|
+
|
|
58
60
|
# Base classes and typing objects.
|
|
59
|
-
DBModel =
|
|
61
|
+
DBModel = type[Base]
|
|
60
62
|
DBEngine = Engine | AsyncEngine
|
|
61
63
|
DBSession = Session | AsyncSession
|
|
62
64
|
|
|
@@ -174,7 +176,7 @@ def create_db_models(metadata: MetaData) -> dict[str, DBModel]:
|
|
|
174
176
|
logger.debug(f"> Creating model for table {table_name}.")
|
|
175
177
|
models[table_name] = type(
|
|
176
178
|
table_name.capitalize(),
|
|
177
|
-
(
|
|
179
|
+
(Base,),
|
|
178
180
|
{"__table__": table},
|
|
179
181
|
)
|
|
180
182
|
|
|
@@ -182,7 +184,7 @@ def create_db_models(metadata: MetaData) -> dict[str, DBModel]:
|
|
|
182
184
|
return models
|
|
183
185
|
|
|
184
186
|
|
|
185
|
-
def create_db_interface(model: DBModel) -> type[
|
|
187
|
+
def create_db_interface(model: DBModel) -> type[PydanticModel]:
|
|
186
188
|
"""Create a Pydantic interface for a SQLAlchemy model.
|
|
187
189
|
|
|
188
190
|
Args:
|
|
@@ -0,0 +1,175 @@
|
|
|
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)
|
|
@@ -58,7 +58,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
|
|
|
58
58
|
Args:
|
|
59
59
|
engine: The database engine used to facilitate database interactions.
|
|
60
60
|
metadata: The metadata object containing the database schema.
|
|
61
|
-
|
|
61
|
+
name: The application name.
|
|
62
62
|
version: The application versionnumber.
|
|
63
63
|
|
|
64
64
|
Returns:
|
|
@@ -89,15 +89,11 @@ def create_model_router(engine: DBEngine, model: DBModel, writeable: bool = Fals
|
|
|
89
89
|
router = APIRouter()
|
|
90
90
|
|
|
91
91
|
# Construct path parameters from primary key columns
|
|
92
|
-
pk_columns = model.__table__.primary_key.columns
|
|
93
|
-
path_params_url = "/".join(f"{{{
|
|
92
|
+
pk_columns = sorted(column.name for column in model.__table__.primary_key.columns)
|
|
93
|
+
path_params_url = "/".join(f"{{{col_name}}}" for col_name in pk_columns)
|
|
94
94
|
path_params_openapi = {
|
|
95
95
|
"parameters": [
|
|
96
|
-
{
|
|
97
|
-
"name": col.name,
|
|
98
|
-
"in": "path",
|
|
99
|
-
"required": True
|
|
100
|
-
} for col in pk_columns
|
|
96
|
+
{"name": col_name, "in": "path", "required": True} for col_name in pk_columns
|
|
101
97
|
]
|
|
102
98
|
}
|
|
103
99
|
|
|
@@ -113,7 +109,6 @@ def create_model_router(engine: DBEngine, model: DBModel, writeable: bool = Fals
|
|
|
113
109
|
endpoint=create_list_records_handler(engine, model),
|
|
114
110
|
status_code=status.HTTP_200_OK,
|
|
115
111
|
tags=[model.__name__],
|
|
116
|
-
openapi_extra=path_params_openapi
|
|
117
112
|
)
|
|
118
113
|
|
|
119
114
|
router.add_api_route(
|
|
@@ -135,7 +130,6 @@ def create_model_router(engine: DBEngine, model: DBModel, writeable: bool = Fals
|
|
|
135
130
|
endpoint=create_post_record_handler(engine, model),
|
|
136
131
|
status_code=status.HTTP_201_CREATED,
|
|
137
132
|
tags=[model.__name__],
|
|
138
|
-
openapi_extra=path_params_openapi
|
|
139
133
|
)
|
|
140
134
|
|
|
141
135
|
router.add_api_route(
|
|
@@ -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.1"
|
|
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" }]
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development",
|
|
25
25
|
"Typing :: Typed"
|
|
26
26
|
]
|
|
27
|
-
requires-python = ">=3.
|
|
27
|
+
requires-python = ">=3.11"
|
|
28
28
|
dependencies = [
|
|
29
29
|
"aiomysql~=0.2",
|
|
30
30
|
"aioodbc~=0.5",
|
|
@@ -1,147 +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. A *get* function is used to parse
|
|
8
|
-
parameters from a FastAPI request and an *apply* function is used to apply
|
|
9
|
-
the arguments onto a SQLAlchemy query.
|
|
10
|
-
|
|
11
|
-
!!! example "Example: Parameter Parsing and Application"
|
|
12
|
-
|
|
13
|
-
When handling URL parameters, a *get* function is injected as a dependency
|
|
14
|
-
into the signature of the request handler. The parsed parameter dictionary
|
|
15
|
-
is then passed to an *apply* function.
|
|
16
|
-
|
|
17
|
-
```python
|
|
18
|
-
from fastapi import FastAPI, Response, Depends
|
|
19
|
-
from sqlalchemy import select
|
|
20
|
-
from auto_rest.query_params import get_pagination_params, apply_pagination_params
|
|
21
|
-
|
|
22
|
-
app = FastAPI()
|
|
23
|
-
|
|
24
|
-
@app.get("/items/")
|
|
25
|
-
async def list_items(
|
|
26
|
-
pagination_params: dict = Depends(get_pagination_params),
|
|
27
|
-
response: Response
|
|
28
|
-
):
|
|
29
|
-
query = select(SomeModel)
|
|
30
|
-
query = apply_pagination_params(query, pagination_params, response)
|
|
31
|
-
return ... # Logic to further process and execute the query goes here
|
|
32
|
-
```
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
from typing import Literal
|
|
36
|
-
|
|
37
|
-
from fastapi import Query
|
|
38
|
-
from sqlalchemy import asc, desc
|
|
39
|
-
from sqlalchemy.sql.selectable import Select
|
|
40
|
-
from starlette.responses import Response
|
|
41
|
-
|
|
42
|
-
__all__ = [
|
|
43
|
-
"apply_ordering_params",
|
|
44
|
-
"apply_pagination_params",
|
|
45
|
-
"get_ordering_params",
|
|
46
|
-
"get_pagination_params",
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_pagination_params(
|
|
51
|
-
_limit_: int = Query(10, ge=0, description="The maximum number of records to return."),
|
|
52
|
-
_offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
|
|
53
|
-
) -> dict[str, int]:
|
|
54
|
-
"""Extract pagination parameters from request query parameters.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
_limit_: The maximum number of records to return.
|
|
58
|
-
_offset_: The starting index of the returned records.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
dict: A dictionary containing the `limit` and `offset` values.
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
return {"limit": _limit_, "offset": _offset_}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def apply_pagination_params(query: Select, params: dict[str, int], response: Response) -> Select:
|
|
68
|
-
"""Apply pagination to a database query.
|
|
69
|
-
|
|
70
|
-
Returns a copy of the provided query with offset and limit parameters applied.
|
|
71
|
-
Compatible with parameters returned by the `get_pagination_params` method.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
query: The database query to apply parameters to.
|
|
75
|
-
params: A dictionary containing parsed URL parameters.
|
|
76
|
-
response: The outgoing HTTP response object.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
A copy of the query modified to only return the paginated values.
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
limit = params.get("limit", 0)
|
|
83
|
-
offset = params.get("offset", 0)
|
|
84
|
-
|
|
85
|
-
if limit < 0 or offset < 0:
|
|
86
|
-
raise ValueError("Pagination parameters must be greater than or equal to zero.")
|
|
87
|
-
|
|
88
|
-
if limit == 0:
|
|
89
|
-
response.headers["X-Pagination-Applied"] = "false"
|
|
90
|
-
return query
|
|
91
|
-
|
|
92
|
-
response.headers["X-Pagination-Applied"] = "true"
|
|
93
|
-
response.headers["X-Pagination-Limit"] = str(limit)
|
|
94
|
-
response.headers["X-Pagination-Offset"] = str(offset)
|
|
95
|
-
return query.offset(offset).limit(limit)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def get_ordering_params(
|
|
99
|
-
_order_by_: str | None = Query(None, description="The field name to sort by."),
|
|
100
|
-
_direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
|
|
101
|
-
) -> dict:
|
|
102
|
-
"""Extract ordering parameters from request query parameters.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
_order_by_: The field to order by.
|
|
106
|
-
_direction_: The direction to order by.
|
|
107
|
-
|
|
108
|
-
Returns:
|
|
109
|
-
dict: A dictionary containing the `order_by` and `direction` values.
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
return {"order_by": _order_by_, "direction": _direction_}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def apply_ordering_params(query: Select, params: dict, response: Response) -> Select:
|
|
116
|
-
"""Apply ordering to a database query.
|
|
117
|
-
|
|
118
|
-
Returns a copy of the provided query with ordering parameters applied.
|
|
119
|
-
Compatible with parameters returned by the `get_ordering_params` method.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
query: The database query to apply parameters to.
|
|
123
|
-
params: A dictionary containing parsed URL parameters.
|
|
124
|
-
response: The outgoing HTTP response object.
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
A copy of the query modified to return ordered values.
|
|
128
|
-
"""
|
|
129
|
-
|
|
130
|
-
order_by = params.get("order_by")
|
|
131
|
-
direction = params.get("direction", "asc")
|
|
132
|
-
|
|
133
|
-
if not order_by:
|
|
134
|
-
response.headers["X-Order-Applied"] = "false"
|
|
135
|
-
return query
|
|
136
|
-
|
|
137
|
-
response.headers["X-Order-Applied"] = "true"
|
|
138
|
-
response.headers["X-Order-By"] = order_by
|
|
139
|
-
response.headers["X-Order-Direction"] = direction
|
|
140
|
-
|
|
141
|
-
if direction == "asc":
|
|
142
|
-
return query.order_by(asc(order_by))
|
|
143
|
-
|
|
144
|
-
if direction == "desc":
|
|
145
|
-
return query.order_by(desc(order_by))
|
|
146
|
-
|
|
147
|
-
raise ValueError("Ordering direction must be 'asc' or 'desc'.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|