auto-rest-api 0.1.4__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-rest-api might be problematic. Click here for more details.
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/PKG-INFO +4 -2
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/__main__.py +1 -1
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/app.py +26 -5
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/cli.py +54 -16
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/handlers.py +18 -11
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/models.py +2 -4
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/queries.py +5 -61
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/routers.py +4 -4
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/pyproject.toml +5 -3
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/LICENSE.md +0 -0
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/README.md +0 -0
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/auto_rest/__init__.py +0 -0
- {auto_rest_api-0.1.4 → auto_rest_api-0.1.5}/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.5
|
|
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,27 @@ 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": f"{request.url.path}?{request.url.query}",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Execute handling logic
|
|
51
|
+
try:
|
|
52
|
+
response = await call_next(request)
|
|
53
|
+
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
logger.error(str(exec), exc_info=exc, extra=request_meta)
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
# Log the outgoing response
|
|
59
|
+
status = HTTPStatus(response.status_code)
|
|
60
|
+
level = logging.INFO if status < 400 else logging.ERROR
|
|
61
|
+
logger.log(level, f"{status} {status.phrase}", extra=request_meta)
|
|
62
|
+
|
|
43
63
|
return response
|
|
44
64
|
|
|
45
65
|
|
|
@@ -66,6 +86,7 @@ def create_app(app_title: str, app_version: str) -> FastAPI:
|
|
|
66
86
|
)
|
|
67
87
|
|
|
68
88
|
app.middleware("http")(logging_middleware)
|
|
89
|
+
app.add_middleware(CorrelationIdMiddleware)
|
|
69
90
|
app.add_middleware(
|
|
70
91
|
CORSMiddleware,
|
|
71
92
|
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"]
|
|
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.
|
|
@@ -191,7 +188,6 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
|
|
|
191
188
|
"""
|
|
192
189
|
|
|
193
190
|
interface = create_interface(table)
|
|
194
|
-
interface_opt = create_interface(table, mode="optional")
|
|
195
191
|
columns = tuple(table.columns.keys())
|
|
196
192
|
|
|
197
193
|
async def list_records_handler(
|
|
@@ -207,14 +203,25 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
|
|
|
207
203
|
URL query parameters are used to enable filtering, ordering, and paginating returned values.
|
|
208
204
|
"""
|
|
209
205
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
# Determine total record count
|
|
207
|
+
total_count_query = select(func.count()).select_from(table)
|
|
208
|
+
total_count = await execute_session_query(session, total_count_query)
|
|
209
|
+
|
|
210
|
+
# Set headers
|
|
211
|
+
response.headers["x-pagination-limit"] = str(_limit_)
|
|
212
|
+
response.headers["x-pagination-offset"] = str(_offset_)
|
|
213
|
+
response.headers["x-pagination-total"] = str(total_count.first()[0])
|
|
214
|
+
response.headers["x-order-by"] = str(_order_by_)
|
|
215
|
+
response.headers["x-order-direction"] = str(_direction_)
|
|
214
216
|
|
|
217
|
+
# Fetch data per the request parameters
|
|
215
218
|
query = select(table)
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
if _limit_ > 0:
|
|
220
|
+
query = query.offset(_offset_).limit(_limit_)
|
|
221
|
+
|
|
222
|
+
if _order_by_ is not None:
|
|
223
|
+
direction = {'desc': desc, 'asc': asc}[_direction_]
|
|
224
|
+
query = query.order_by(direction(_order_by_))
|
|
218
225
|
|
|
219
226
|
result = await execute_session_query(session, query)
|
|
220
227
|
return [row._mapping for row in result.all()]
|
|
@@ -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.")
|
|
@@ -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.5"
|
|
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
|