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