auto-rest-api 0.1.4__tar.gz → 0.1.6__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.4 → auto_rest_api-0.1.6}/PKG-INFO +4 -2
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/__main__.py +1 -1
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/app.py +29 -5
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/cli.py +54 -16
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/handlers.py +37 -18
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/models.py +6 -8
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/queries.py +5 -61
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/routers.py +4 -4
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/pyproject.toml +5 -3
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/LICENSE.md +0 -0
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/README.md +0 -0
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/__init__.py +0 -0
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.6}/auto_rest/interfaces.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.6
|
|
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.11
|
|
8
|
+
Requires-Python: >=3.11,<4
|
|
9
9
|
Classifier: Environment :: Web Environment
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -21,7 +21,9 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Dist: aiomysql (>=0.2,<1.0)
|
|
22
22
|
Requires-Dist: aioodbc (>=0.5,<1.0)
|
|
23
23
|
Requires-Dist: aiosqlite (>=0.20,<1.0)
|
|
24
|
+
Requires-Dist: asgi-correlation-id (>=4.3.4,<5.0.0)
|
|
24
25
|
Requires-Dist: asyncpg (>=0.30,<1.0)
|
|
26
|
+
Requires-Dist: colorlog (>=6.9.0,<7.0.0)
|
|
25
27
|
Requires-Dist: fastapi (>=0.115,<1.0)
|
|
26
28
|
Requires-Dist: greenlet (>=3.1,<4.0)
|
|
27
29
|
Requires-Dist: httpx (>=0.28,<1.0)
|
|
@@ -15,19 +15,21 @@ deploying Fast-API applications.
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
+
from http import HTTPStatus
|
|
18
19
|
|
|
19
20
|
import uvicorn
|
|
21
|
+
from asgi_correlation_id import CorrelationIdMiddleware
|
|
20
22
|
from fastapi import FastAPI, Request
|
|
21
23
|
from fastapi.middleware.cors import CORSMiddleware
|
|
22
24
|
from starlette.responses import Response
|
|
23
25
|
|
|
24
26
|
__all__ = ["create_app", "run_server"]
|
|
25
27
|
|
|
26
|
-
logger = logging.getLogger("
|
|
28
|
+
logger = logging.getLogger("auto_rest.access")
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
async def logging_middleware(request: Request, call_next: callable) -> Response:
|
|
30
|
-
"""FastAPI middleware for
|
|
32
|
+
"""FastAPI middleware for logging response status codes.
|
|
31
33
|
|
|
32
34
|
Args:
|
|
33
35
|
request: The incoming HTTP request.
|
|
@@ -37,9 +39,30 @@ async def logging_middleware(request: Request, call_next: callable) -> Response:
|
|
|
37
39
|
The outgoing HTTP response.
|
|
38
40
|
"""
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
# Extract metadata from the request
|
|
43
|
+
request_meta = {
|
|
44
|
+
"ip": request.client.host,
|
|
45
|
+
"port": request.client.port,
|
|
46
|
+
"method": request.method,
|
|
47
|
+
"endpoint": request.url.path,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if request.url.query:
|
|
51
|
+
request_meta["endpoint"] += "?" + request.url.query
|
|
52
|
+
|
|
53
|
+
# Execute handling logic
|
|
54
|
+
try:
|
|
55
|
+
response = await call_next(request)
|
|
56
|
+
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
logger.error(str(exec), exc_info=exc, extra=request_meta)
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
# Log the outgoing response
|
|
62
|
+
status = HTTPStatus(response.status_code)
|
|
63
|
+
level = logging.INFO if status < 400 else logging.ERROR
|
|
64
|
+
logger.log(level, f"{status} {status.phrase}", extra=request_meta)
|
|
65
|
+
|
|
43
66
|
return response
|
|
44
67
|
|
|
45
68
|
|
|
@@ -66,6 +89,7 @@ def create_app(app_title: str, app_version: str) -> FastAPI:
|
|
|
66
89
|
)
|
|
67
90
|
|
|
68
91
|
app.middleware("http")(logging_middleware)
|
|
92
|
+
app.add_middleware(CorrelationIdMiddleware)
|
|
69
93
|
app.add_middleware(
|
|
70
94
|
CORSMiddleware,
|
|
71
95
|
allow_credentials=True,
|
|
@@ -31,13 +31,11 @@ Python `logging` library.
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
import importlib.metadata
|
|
34
|
-
import logging
|
|
34
|
+
import logging.config
|
|
35
35
|
from argparse import ArgumentParser, HelpFormatter
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
__all__ = ["VERSION", "configure_cli_logging", "create_cli_parser"]
|
|
38
|
+
__all__ = ["configure_cli_logging", "create_cli_parser", "VERSION"]
|
|
41
39
|
|
|
42
40
|
VERSION = importlib.metadata.version("auto-rest-api")
|
|
43
41
|
|
|
@@ -49,7 +47,7 @@ def configure_cli_logging(level: str) -> None:
|
|
|
49
47
|
logging configurations.
|
|
50
48
|
|
|
51
49
|
Args:
|
|
52
|
-
level: The Python logging level.
|
|
50
|
+
level: The Python logging level (e.g., "DEBUG", "INFO", etc.).
|
|
53
51
|
"""
|
|
54
52
|
|
|
55
53
|
# Normalize and validate the logging level.
|
|
@@ -57,17 +55,57 @@ def configure_cli_logging(level: str) -> None:
|
|
|
57
55
|
if level not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
|
58
56
|
raise ValueError(f"Invalid logging level: {level}")
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
msg_prefix = "%(log_color)s%(levelname)-8s%(reset)s (%(asctime)s) [%(correlation_id)s] "
|
|
59
|
+
logging.config.dictConfig({
|
|
60
|
+
"version": 1,
|
|
61
|
+
"disable_existing_loggers": True,
|
|
62
|
+
"filters": {
|
|
63
|
+
"correlation_id": {
|
|
64
|
+
"()": "asgi_correlation_id.CorrelationIdFilter",
|
|
65
|
+
"uuid_length": 8,
|
|
66
|
+
"default_value": "-" * 8
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"formatters": {
|
|
70
|
+
"app": {
|
|
71
|
+
"()": "colorlog.ColoredFormatter",
|
|
72
|
+
"format": msg_prefix + "%(message)s",
|
|
73
|
+
},
|
|
74
|
+
"access": {
|
|
75
|
+
"()": "colorlog.ColoredFormatter",
|
|
76
|
+
"format": msg_prefix + "%(ip)s:%(port)s - %(method)s %(endpoint)s - %(message)s",
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"handlers": {
|
|
80
|
+
"app": {
|
|
81
|
+
"class": "colorlog.StreamHandler",
|
|
82
|
+
"formatter": "app",
|
|
83
|
+
"filters": ["correlation_id"],
|
|
84
|
+
},
|
|
85
|
+
"access": {
|
|
86
|
+
"class": "colorlog.StreamHandler",
|
|
87
|
+
"formatter": "access",
|
|
88
|
+
"filters": ["correlation_id"],
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"loggers": {
|
|
92
|
+
"auto_rest": {
|
|
93
|
+
"handlers": ["app"],
|
|
94
|
+
"level": level,
|
|
95
|
+
"propagate": False
|
|
96
|
+
},
|
|
97
|
+
"auto_rest.access": {
|
|
98
|
+
"handlers": ["access"],
|
|
99
|
+
"level": level,
|
|
100
|
+
"propagate": False
|
|
101
|
+
},
|
|
102
|
+
"auto_rest.query": {
|
|
103
|
+
"handlers": ["app"],
|
|
104
|
+
"level": level,
|
|
105
|
+
"propagate": False
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
71
109
|
|
|
72
110
|
|
|
73
111
|
def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
|
|
@@ -50,13 +50,12 @@ This makes it easy to incorporate handlers into a FastAPI application.
|
|
|
50
50
|
should always be tested within the context of a FastAPI application.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
import logging
|
|
54
53
|
from typing import Awaitable, Callable, Literal, Optional
|
|
55
54
|
|
|
56
55
|
from fastapi import Depends, Query, Response
|
|
57
56
|
from pydantic import create_model
|
|
58
57
|
from pydantic.main import BaseModel as PydanticModel
|
|
59
|
-
from sqlalchemy import insert, MetaData, select, Table
|
|
58
|
+
from sqlalchemy import asc, desc, func, insert, MetaData, select, Table
|
|
60
59
|
|
|
61
60
|
from .interfaces import *
|
|
62
61
|
from .models import *
|
|
@@ -75,8 +74,6 @@ __all__ = [
|
|
|
75
74
|
"create_welcome_handler",
|
|
76
75
|
]
|
|
77
76
|
|
|
78
|
-
logger = logging.getLogger("auto-rest")
|
|
79
|
-
|
|
80
77
|
|
|
81
78
|
def create_welcome_handler() -> Callable[[], Awaitable[PydanticModel]]:
|
|
82
79
|
"""Create an endpoint handler that returns an application welcome message.
|
|
@@ -192,32 +189,54 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
|
|
|
192
189
|
|
|
193
190
|
interface = create_interface(table)
|
|
194
191
|
interface_opt = create_interface(table, mode="optional")
|
|
195
|
-
|
|
192
|
+
col_names = tuple(table.columns.keys())
|
|
196
193
|
|
|
197
194
|
async def list_records_handler(
|
|
198
195
|
response: Response,
|
|
199
196
|
session: DBSession = Depends(create_session_iterator(engine)),
|
|
197
|
+
filters: interface_opt = Depends(),
|
|
200
198
|
_limit_: int = Query(0, ge=0, description="The maximum number of records to return."),
|
|
201
199
|
_offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
|
|
202
|
-
_order_by_: Optional[Literal[*
|
|
203
|
-
_direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
|
|
200
|
+
_order_by_: Optional[Literal[*col_names]] = Query(None, description="The field name to sort by."),
|
|
201
|
+
_direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order."),
|
|
204
202
|
) -> list[interface]:
|
|
205
203
|
"""Fetch a list of records from the database.
|
|
206
204
|
|
|
207
205
|
URL query parameters are used to enable filtering, ordering, and paginating returned values.
|
|
208
206
|
"""
|
|
209
207
|
|
|
210
|
-
response.headers["X-Pagination-Limit"] = str(_limit_)
|
|
211
|
-
response.headers["X-Pagination-Offset"] = str(_offset_)
|
|
212
|
-
response.headers["X-Order-By"] = str(_order_by_)
|
|
213
|
-
response.headers["X-Order-Direction"] = str(_direction_)
|
|
214
|
-
|
|
215
208
|
query = select(table)
|
|
216
|
-
query = apply_pagination_params(query, _limit_, _offset_)
|
|
217
|
-
query = apply_ordering_params(query, _order_by_, _direction_)
|
|
218
209
|
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
# Fetch data per the request parameters
|
|
211
|
+
for param, value in filters:
|
|
212
|
+
if value is not None:
|
|
213
|
+
column = getattr(table.c, param)
|
|
214
|
+
if value.lower() == "_null_":
|
|
215
|
+
query = query.filter(column.is_(None))
|
|
216
|
+
|
|
217
|
+
else:
|
|
218
|
+
query = query.filter(column.ilike(f"%{value}%"))
|
|
219
|
+
|
|
220
|
+
if _limit_ > 0:
|
|
221
|
+
query = query.offset(_offset_).limit(_limit_)
|
|
222
|
+
|
|
223
|
+
if _order_by_ is not None:
|
|
224
|
+
direction = {'desc': desc, 'asc': asc}[_direction_]
|
|
225
|
+
query = query.order_by(direction(_order_by_))
|
|
226
|
+
|
|
227
|
+
# Determine total record count
|
|
228
|
+
total_count_query = select(func.count()).select_from(table)
|
|
229
|
+
total_count = await execute_session_query(session, total_count_query)
|
|
230
|
+
|
|
231
|
+
# Set headers
|
|
232
|
+
response.headers["x-pagination-limit"] = str(_limit_)
|
|
233
|
+
response.headers["x-pagination-offset"] = str(_offset_)
|
|
234
|
+
response.headers["x-pagination-total"] = str(total_count.first()[0])
|
|
235
|
+
response.headers["x-order-by"] = str(_order_by_)
|
|
236
|
+
response.headers["x-order-direction"] = str(_direction_)
|
|
237
|
+
|
|
238
|
+
# noinspection PyTypeChecker
|
|
239
|
+
return await execute_session_query(session, query)
|
|
221
240
|
|
|
222
241
|
return list_records_handler
|
|
223
242
|
|
|
@@ -250,7 +269,7 @@ def create_get_record_handler(engine: DBEngine, table: Table) -> Callable[..., A
|
|
|
250
269
|
return get_record_handler
|
|
251
270
|
|
|
252
271
|
|
|
253
|
-
def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[
|
|
272
|
+
def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[None]]:
|
|
254
273
|
"""Create a function for handling POST requests against a record in the database.
|
|
255
274
|
|
|
256
275
|
Args:
|
|
@@ -269,7 +288,7 @@ def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[...,
|
|
|
269
288
|
) -> None:
|
|
270
289
|
"""Create a new record in the database."""
|
|
271
290
|
|
|
272
|
-
query = insert(table).values(**data.
|
|
291
|
+
query = insert(table).values(**data.model_dump())
|
|
273
292
|
await execute_session_query(session, query)
|
|
274
293
|
await commit_session(session)
|
|
275
294
|
|
|
@@ -32,7 +32,7 @@ connection and session handling are configured accordingly.
|
|
|
32
32
|
import asyncio
|
|
33
33
|
import logging
|
|
34
34
|
from pathlib import Path
|
|
35
|
-
from typing import Callable
|
|
35
|
+
from typing import AsyncGenerator, Callable, Generator
|
|
36
36
|
|
|
37
37
|
import yaml
|
|
38
38
|
from sqlalchemy import create_engine, Engine, MetaData, URL
|
|
@@ -49,7 +49,7 @@ __all__ = [
|
|
|
49
49
|
"parse_db_settings"
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
logger = logging.getLogger("
|
|
52
|
+
logger = logging.getLogger("auto_rest")
|
|
53
53
|
|
|
54
54
|
# Base classes and typing objects.
|
|
55
55
|
DBEngine = Engine | AsyncEngine
|
|
@@ -70,7 +70,7 @@ def parse_db_settings(path: Path | None) -> dict[str, any]:
|
|
|
70
70
|
logger.debug(f"Parsing engine configuration from {path}.")
|
|
71
71
|
return yaml.safe_load(path.read_text()) or dict()
|
|
72
72
|
|
|
73
|
-
logger.debug("No
|
|
73
|
+
logger.debug("No configuration file specified.")
|
|
74
74
|
return {}
|
|
75
75
|
|
|
76
76
|
|
|
@@ -129,8 +129,6 @@ def create_db_engine(url: URL, **kwargs: dict[str: any]) -> DBEngine:
|
|
|
129
129
|
A SQLAlchemy `Engine` or `AsyncEngine` instance.
|
|
130
130
|
"""
|
|
131
131
|
|
|
132
|
-
logger.debug(f"Building database engine for {url}.")
|
|
133
|
-
|
|
134
132
|
if url.get_dialect().is_async:
|
|
135
133
|
engine = create_async_engine(url, **kwargs)
|
|
136
134
|
logger.debug("Asynchronous connection established.")
|
|
@@ -171,7 +169,7 @@ def create_db_metadata(engine: DBEngine) -> MetaData:
|
|
|
171
169
|
return metadata
|
|
172
170
|
|
|
173
171
|
|
|
174
|
-
def create_session_iterator(engine: DBEngine) -> Callable[[],
|
|
172
|
+
def create_session_iterator(engine: DBEngine) -> Callable[[], Generator[Session, None, None] | AsyncGenerator[AsyncSession, None]]:
|
|
175
173
|
"""Create a generator for database sessions.
|
|
176
174
|
|
|
177
175
|
Returns a synchronous or asynchronous function depending on whether
|
|
@@ -187,12 +185,12 @@ def create_session_iterator(engine: DBEngine) -> Callable[[], DBSession]:
|
|
|
187
185
|
"""
|
|
188
186
|
|
|
189
187
|
if isinstance(engine, AsyncEngine):
|
|
190
|
-
async def session_iterator() -> AsyncSession:
|
|
188
|
+
async def session_iterator() -> AsyncGenerator[AsyncSession, None]:
|
|
191
189
|
async with AsyncSession(bind=engine, autocommit=False, autoflush=True) as session:
|
|
192
190
|
yield session
|
|
193
191
|
|
|
194
192
|
else:
|
|
195
|
-
def session_iterator() -> Session:
|
|
193
|
+
def session_iterator() -> Generator[Session, None, None]:
|
|
196
194
|
with Session(bind=engine, autocommit=False, autoflush=True) as session:
|
|
197
195
|
yield session
|
|
198
196
|
|
|
@@ -20,81 +20,23 @@ handling and provides a streamlined interface for database interactions.
|
|
|
20
20
|
```
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
import logging
|
|
24
24
|
|
|
25
25
|
from fastapi import HTTPException
|
|
26
|
-
from sqlalchemy import
|
|
26
|
+
from sqlalchemy import Executable, Result
|
|
27
27
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
28
28
|
from starlette import status
|
|
29
29
|
|
|
30
30
|
from auto_rest.models import DBSession
|
|
31
31
|
|
|
32
32
|
__all__ = [
|
|
33
|
-
"apply_ordering_params",
|
|
34
|
-
"apply_pagination_params",
|
|
35
33
|
"commit_session",
|
|
36
34
|
"delete_session_record",
|
|
37
35
|
"execute_session_query",
|
|
38
36
|
"get_record_or_404"
|
|
39
37
|
]
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
def apply_ordering_params(
|
|
43
|
-
query: Select,
|
|
44
|
-
order_by: str | None = None,
|
|
45
|
-
direction: Literal["desc", "asc"] = "asc"
|
|
46
|
-
) -> Select:
|
|
47
|
-
"""Apply ordering to a database query.
|
|
48
|
-
|
|
49
|
-
Returns a copy of the provided query with ordering parameters applied.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
query: The database query to apply parameters to.
|
|
53
|
-
order_by: The name of the column to order by.
|
|
54
|
-
direction: The direction to order by (defaults to "asc").
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
A copy of the query modified to return ordered values.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
if order_by is None:
|
|
61
|
-
return query
|
|
62
|
-
|
|
63
|
-
if order_by not in query.columns:
|
|
64
|
-
raise ValueError(f"Invalid column name: {order_by}")
|
|
65
|
-
|
|
66
|
-
# Default to ascending order for an invalid ordering direction
|
|
67
|
-
if direction == "desc":
|
|
68
|
-
return query.order_by(desc(order_by))
|
|
69
|
-
|
|
70
|
-
elif direction == "asc":
|
|
71
|
-
return query.order_by(asc(order_by))
|
|
72
|
-
|
|
73
|
-
raise ValueError(f"Invalid direction, use 'asc' or 'desc': {direction}")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def apply_pagination_params(query: Select, limit: int = 0, offset: int = 0) -> Select:
|
|
77
|
-
"""Apply pagination to a database query.
|
|
78
|
-
|
|
79
|
-
Returns a copy of the provided query with offset and limit parameters applied.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
query: The database query to apply parameters to.
|
|
83
|
-
limit: The number of results to return.
|
|
84
|
-
offset: The offset to start with.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
A copy of the query modified to only return the paginated values.
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
if offset < 0 or limit < 0:
|
|
91
|
-
raise ValueError("Pagination parameters cannot be negative")
|
|
92
|
-
|
|
93
|
-
# Do not apply pagination if not requested
|
|
94
|
-
if limit == 0:
|
|
95
|
-
return query
|
|
96
|
-
|
|
97
|
-
return query.offset(offset or 0).limit(limit)
|
|
39
|
+
logger = logging.getLogger("auto_rest.query")
|
|
98
40
|
|
|
99
41
|
|
|
100
42
|
async def commit_session(session: DBSession) -> None:
|
|
@@ -124,6 +66,7 @@ async def delete_session_record(session: DBSession, record: Result) -> None:
|
|
|
124
66
|
record: The record to be deleted.
|
|
125
67
|
"""
|
|
126
68
|
|
|
69
|
+
logger.debug("Deleting record.")
|
|
127
70
|
if isinstance(session, AsyncSession):
|
|
128
71
|
await session.delete(record)
|
|
129
72
|
|
|
@@ -144,6 +87,7 @@ async def execute_session_query(session: DBSession, query: Executable) -> Result
|
|
|
144
87
|
The result of the executed query.
|
|
145
88
|
"""
|
|
146
89
|
|
|
90
|
+
logger.debug(str(query).replace("\n", " "))
|
|
147
91
|
if isinstance(session, AsyncSession):
|
|
148
92
|
return await session.execute(query)
|
|
149
93
|
|
|
@@ -38,7 +38,7 @@ __all__ = [
|
|
|
38
38
|
"create_welcome_router",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
-
logger = logging.getLogger("
|
|
41
|
+
logger = logging.getLogger("auto_rest")
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def create_welcome_router() -> APIRouter:
|
|
@@ -83,7 +83,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
|
|
|
83
83
|
tags = ["Application Metadata"]
|
|
84
84
|
|
|
85
85
|
router.add_api_route(
|
|
86
|
-
path="/app",
|
|
86
|
+
path="/app/",
|
|
87
87
|
methods=["GET"],
|
|
88
88
|
endpoint=create_about_handler(name, version),
|
|
89
89
|
summary="Fetch application metadata.",
|
|
@@ -91,7 +91,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
router.add_api_route(
|
|
94
|
-
path="/engine",
|
|
94
|
+
path="/engine/",
|
|
95
95
|
methods=["GET"],
|
|
96
96
|
endpoint=create_engine_handler(engine),
|
|
97
97
|
summary="Fetch database metadata.",
|
|
@@ -99,7 +99,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
router.add_api_route(
|
|
102
|
-
path="/schema",
|
|
102
|
+
path="/schema/",
|
|
103
103
|
methods=["GET"],
|
|
104
104
|
endpoint=create_schema_handler(metadata),
|
|
105
105
|
summary="Fetch the database schema.",
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "auto-rest-api"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
description = "Automatically map database schemas and deploy per-table REST API endpoints."
|
|
10
10
|
authors = [{ name = "Better HPC LLC" }]
|
|
@@ -24,20 +24,22 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development",
|
|
25
25
|
"Typing :: Typed"
|
|
26
26
|
]
|
|
27
|
-
requires-python = ">=3.11"
|
|
27
|
+
requires-python = ">=3.11,<4"
|
|
28
28
|
dependencies = [
|
|
29
29
|
"aiomysql~=0.2",
|
|
30
30
|
"aioodbc~=0.5",
|
|
31
31
|
"aiosqlite~=0.20",
|
|
32
|
+
"asgi-correlation-id (>=4.3.4,<5.0.0)",
|
|
32
33
|
"asyncpg~=0.30",
|
|
34
|
+
"colorlog (>=6.9.0,<7.0.0)",
|
|
33
35
|
"fastapi~=0.115",
|
|
34
36
|
"greenlet~=3.1",
|
|
35
37
|
"httpx~=0.28",
|
|
36
38
|
"oracledb~=2.5",
|
|
37
39
|
"pydantic~=2.10",
|
|
40
|
+
"pyyaml (>=6.0.2,<7.0.0)",
|
|
38
41
|
"sqlalchemy~=2.0",
|
|
39
42
|
"uvicorn~=0.34",
|
|
40
|
-
"pyyaml (>=6.0.2,<7.0.0)",
|
|
41
43
|
]
|
|
42
44
|
|
|
43
45
|
[tool.poetry]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|