auto-rest-api 0.1.1__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 +7 -5
- auto_rest/handlers.py +47 -43
- auto_rest/interfaces.py +126 -0
- auto_rest/models.py +6 -59
- auto_rest/params.py +12 -13
- auto_rest/queries.py +4 -4
- auto_rest/routers.py +94 -73
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.2.dist-info}/METADATA +1 -1
- auto_rest_api-0.1.2.dist-info/RECORD +15 -0
- auto_rest_api-0.1.1.dist-info/RECORD +0 -14
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.2.dist-info}/LICENSE.md +0 -0
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.2.dist-info}/WHEEL +0 -0
- {auto_rest_api-0.1.1.dist-info → auto_rest_api-0.1.2.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
|
@@ -56,9 +56,9 @@ from typing import Awaitable, Callable
|
|
|
56
56
|
from fastapi import Depends, 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
63
|
from .params import *
|
|
64
64
|
from .queries import *
|
|
@@ -180,120 +180,121 @@ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[Pydantic
|
|
|
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] = create_pagination_dependency(
|
|
200
|
-
ordering_params: dict[str, int] = create_ordering_dependency(
|
|
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,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/params.py
CHANGED
|
@@ -29,16 +29,15 @@ validated arguments onto a SQLAlchemy query and returns the updated query.
|
|
|
29
29
|
return ... # Logic to further process and execute the query goes here
|
|
30
30
|
```
|
|
31
31
|
"""
|
|
32
|
+
|
|
32
33
|
from collections.abc import Callable
|
|
33
|
-
from typing import Literal
|
|
34
|
+
from typing import Literal, Optional
|
|
34
35
|
|
|
35
36
|
from fastapi import Depends, Query
|
|
36
|
-
from sqlalchemy import asc, desc
|
|
37
|
+
from sqlalchemy import asc, desc, Table
|
|
37
38
|
from sqlalchemy.sql.selectable import Select
|
|
38
39
|
from starlette.responses import Response
|
|
39
40
|
|
|
40
|
-
from .models import DBModel
|
|
41
|
-
|
|
42
41
|
__all__ = [
|
|
43
42
|
"apply_ordering_params",
|
|
44
43
|
"apply_pagination_params",
|
|
@@ -47,21 +46,21 @@ __all__ = [
|
|
|
47
46
|
]
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
def create_ordering_dependency(
|
|
49
|
+
def create_ordering_dependency(table: Table) -> Callable[..., dict]:
|
|
51
50
|
"""Create an injectable dependency for fetching ordering arguments from query parameters.
|
|
52
51
|
|
|
53
52
|
Args:
|
|
54
|
-
|
|
53
|
+
table: The database table to create the dependency for.
|
|
55
54
|
|
|
56
55
|
Returns:
|
|
57
56
|
An injectable FastAPI dependency.
|
|
58
57
|
"""
|
|
59
58
|
|
|
60
|
-
columns = tuple(
|
|
59
|
+
columns = tuple(table.columns.keys())
|
|
61
60
|
|
|
62
61
|
def get_ordering_params(
|
|
63
|
-
_order_by_: Literal[*columns] = Query(None, description="The field name to sort by."),
|
|
64
|
-
_direction_: Literal["asc", "desc"] = Query(
|
|
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.")
|
|
65
64
|
) -> dict:
|
|
66
65
|
"""Extract ordering parameters from request query parameters.
|
|
67
66
|
|
|
@@ -114,19 +113,19 @@ def apply_ordering_params(query: Select, params: dict, response: Response) -> Se
|
|
|
114
113
|
return query.order_by(asc(order_by))
|
|
115
114
|
|
|
116
115
|
|
|
117
|
-
def create_pagination_dependency(
|
|
116
|
+
def create_pagination_dependency(table: Table) -> Callable[..., dict]:
|
|
118
117
|
"""Create an injectable dependency for fetching pagination arguments from query parameters.
|
|
119
118
|
|
|
120
119
|
Args:
|
|
121
|
-
|
|
120
|
+
table: The database table to create the dependency for.
|
|
122
121
|
|
|
123
122
|
Returns:
|
|
124
123
|
An injectable FastAPI dependency.
|
|
125
124
|
"""
|
|
126
125
|
|
|
127
126
|
def get_pagination_params(
|
|
128
|
-
_limit_: int = Query(
|
|
129
|
-
_offset_: int = Query(
|
|
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."),
|
|
130
129
|
) -> dict[str, int]:
|
|
131
130
|
"""Extract pagination parameters from request query parameters.
|
|
132
131
|
|
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
|
|
|
@@ -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,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,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
|