auto-rest-api 0.1.0__tar.gz → 0.1.2__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.
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/PKG-INFO +2 -2
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/auto_rest/__main__.py +10 -9
- auto_rest_api-0.1.2/auto_rest/app.py +67 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/auto_rest/handlers.py +53 -49
- auto_rest_api-0.1.2/auto_rest/interfaces.py +126 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/auto_rest/models.py +6 -57
- auto_rest_api-0.1.2/auto_rest/params.py +174 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/auto_rest/queries.py +4 -4
- auto_rest_api-0.1.2/auto_rest/routers.py +183 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/pyproject.toml +2 -2
- auto_rest_api-0.1.0/auto_rest/params.py +0 -147
- auto_rest_api-0.1.0/auto_rest/routers.py +0 -168
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/LICENSE.md +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/README.md +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/auto_rest/__init__.py +0 -0
- {auto_rest_api-0.1.0 → auto_rest_api-0.1.2}/auto_rest/cli.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.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
|
|
@@ -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)
|
|
@@ -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,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
|
|
|
@@ -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)
|
|
@@ -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
|
|