auto-rest-api 0.1.3__py3-none-any.whl → 0.1.5__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 CHANGED
@@ -1,9 +1,6 @@
1
1
  """Application entrypoint triggered by calling the packaged CLI command."""
2
2
 
3
3
  import logging
4
- from pathlib import Path
5
-
6
- import yaml
7
4
 
8
5
  from .app import *
9
6
  from .cli import *
@@ -12,19 +9,18 @@ from .routers import *
12
9
 
13
10
  __all__ = ["main", "run_application"]
14
11
 
15
- logger = logging.getLogger(__name__)
12
+ logger = logging.getLogger("auto_rest")
16
13
 
17
14
 
18
15
  def main() -> None: # pragma: no cover
19
- """Parse command-line arguments and launch an API server."""
16
+ """Application entry point called when executing the command line interface.
20
17
 
21
- try:
22
- parser = create_cli_parser()
23
- args = vars(parser.parse_args())
24
- log_level = args.pop("log_level")
18
+ This is a wrapper around the `run_application` function used to provide
19
+ graceful error handling.
20
+ """
25
21
 
26
- configure_cli_logging(log_level)
27
- run_application(**args)
22
+ try:
23
+ run_application()
28
24
 
29
25
  except KeyboardInterrupt:
30
26
  pass
@@ -33,62 +29,42 @@ def main() -> None: # pragma: no cover
33
29
  logger.critical(str(e), exc_info=True)
34
30
 
35
31
 
36
- def run_application(
37
- enable_docs: bool,
38
- enable_write: bool,
39
- db_driver: str,
40
- db_host: str,
41
- db_port: int,
42
- db_name: str,
43
- db_user: str,
44
- db_pass: str,
45
- db_config: Path | None,
46
- server_host: str,
47
- server_port: int,
48
- app_title: str,
49
- app_version: str,
50
- ) -> None: # pragma: no cover
32
+ def run_application(cli_args: list[str] = None, /) -> None: # pragma: no cover
51
33
  """Run an Auto-REST API server.
52
34
 
53
35
  This function is equivalent to launching an API server from the command line
54
- and accepts the same arguments as those provided in the CLI.
36
+ and accepts the same arguments as those provided in the CLI. Arguments are
37
+ parsed from STDIN by default, unless specified in the function call.
55
38
 
56
39
  Args:
57
- enable_docs: Whether to enable the 'docs' API endpoint.
58
- enable_write: Whether to enable support for write operations.
59
- db_driver: SQLAlchemy-compatible database driver.
60
- db_host: Database host address.
61
- db_port: Database port number.
62
- db_name: Database name.
63
- db_user: Database authentication username.
64
- db_pass: Database authentication password.
65
- db_config: Path to a database configuration file.
66
- server_host: API server host address.
67
- server_port: API server port number.
68
- app_title: title for the generated OpenAPI schema.
69
- app_version: version number for the generated OpenAPI schema.
40
+ A list of commandline arguments used to run the application.
70
41
  """
71
42
 
72
- logger.info(f"Mapping database schema for {db_name}.")
73
-
74
- # Resolve database connection settings
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
- db_kwargs = yaml.safe_load(db_config.read_text()) if db_config else {}
77
-
78
- # Connect to and map the database.
43
+ # Parse application arguments
44
+ args = create_cli_parser().parse_args(cli_args)
45
+ configure_cli_logging(args.log_level)
46
+
47
+ logger.info(f"Resolving database connection settings.")
48
+ db_kwargs = parse_db_settings(args.db_config)
49
+ db_url = create_db_url(
50
+ driver=args.db_driver,
51
+ host=args.db_host,
52
+ port=args.db_port,
53
+ database=args.db_name,
54
+ username=args.db_user,
55
+ password=args.db_pass
56
+ )
57
+
58
+ logger.info("Mapping database schema.")
79
59
  db_conn = create_db_engine(db_url, **db_kwargs)
80
60
  db_meta = create_db_metadata(db_conn)
81
61
 
82
- # Build an empty application and dynamically add the requested functionality.
83
- logger.info("Creating API application.")
84
- app = create_app(app_title, app_version, enable_docs)
62
+ logger.info("Creating application.")
63
+ app = create_app(args.app_title, args.app_version)
85
64
  app.include_router(create_welcome_router(), prefix="")
86
- app.include_router(create_meta_router(db_conn, db_meta, app_title, app_version), prefix="/meta")
87
-
65
+ app.include_router(create_meta_router(db_conn, db_meta, args.app_title, args.app_version), prefix="/meta")
88
66
  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}")
67
+ app.include_router(create_table_router(db_conn, table), prefix=f"/db/{table_name}")
91
68
 
92
- # Launch the API server.
93
- logger.info(f"Launching API server on http://{server_host}:{server_port}.")
94
- run_server(app, server_host, server_port)
69
+ logger.info(f"Launching server on http://{args.server_host}:{args.server_port}.")
70
+ run_server(app, args.server_host, args.server_port)
auto_rest/app.py CHANGED
@@ -8,20 +8,62 @@ deploying Fast-API applications.
8
8
  ```python
9
9
  from auto_rest.app import create_app, run_server
10
10
 
11
- app = create_app(app_title="My Application", app_version="1.2.3", enable_docs=True)
11
+ app = create_app(app_title="My Application", app_version="1.2.3")
12
12
  ... # Add endpoints to the application here
13
13
  run_server(app, host="127.0.0.1", port=8081)
14
14
  ```
15
15
  """
16
16
 
17
+ import logging
18
+ from http import HTTPStatus
19
+
17
20
  import uvicorn
18
- from fastapi import FastAPI
21
+ from asgi_correlation_id import CorrelationIdMiddleware
22
+ from fastapi import FastAPI, Request
19
23
  from fastapi.middleware.cors import CORSMiddleware
24
+ from starlette.responses import Response
20
25
 
21
26
  __all__ = ["create_app", "run_server"]
22
27
 
28
+ logger = logging.getLogger("auto_rest.access")
29
+
30
+
31
+ async def logging_middleware(request: Request, call_next: callable) -> Response:
32
+ """FastAPI middleware for logging response status codes.
33
+
34
+ Args:
35
+ request: The incoming HTTP request.
36
+ call_next: The next middleware in the middleware chain.
37
+
38
+ Returns:
39
+ The outgoing HTTP response.
40
+ """
41
+
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)
23
62
 
24
- def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
63
+ return response
64
+
65
+
66
+ def create_app(app_title: str, app_version: str) -> FastAPI:
25
67
  """Create and configure a FastAPI application instance.
26
68
 
27
69
  This function initializes a FastAPI app with a customizable title, version,
@@ -31,7 +73,6 @@ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
31
73
  Args:
32
74
  app_title: The title of the FastAPI application.
33
75
  app_version: The version of the FastAPI application.
34
- enable_docs: Whether to enable the `/docs/` endpoint.
35
76
 
36
77
  Returns:
37
78
  FastAPI: A configured FastAPI application instance.
@@ -40,14 +81,16 @@ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
40
81
  app = FastAPI(
41
82
  title=app_title,
42
83
  version=app_version,
43
- docs_url="/docs/" if enable_docs else None,
84
+ docs_url="/docs/",
44
85
  redoc_url=None,
45
86
  )
46
87
 
88
+ app.middleware("http")(logging_middleware)
89
+ app.add_middleware(CorrelationIdMiddleware)
47
90
  app.add_middleware(
48
91
  CORSMiddleware,
49
- allow_origins=["*"],
50
92
  allow_credentials=True,
93
+ allow_origins=["*"],
51
94
  allow_methods=["*"],
52
95
  allow_headers=["*"],
53
96
  )
@@ -55,7 +98,7 @@ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
55
98
  return app
56
99
 
57
100
 
58
- def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
101
+ def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
59
102
  """Deploy a FastAPI application server.
60
103
 
61
104
  Args:
@@ -64,4 +107,5 @@ def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
64
107
  port: The port number for the server to listen on.
65
108
  """
66
109
 
67
- uvicorn.run(app, host=host, port=port, log_level="error")
110
+ # Uvicorn overwrites its logging level when run and needs to be manually disabled here.
111
+ uvicorn.run(app, host=host, port=port, log_level=1000)
auto_rest/cli.py CHANGED
@@ -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
- from uvicorn.logging import DefaultFormatter
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,15 +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
- # Set up logging with a stream handler.
61
- handler = logging.StreamHandler()
62
- handler.setFormatter(DefaultFormatter(fmt="%(levelprefix)s %(message)s"))
63
- logging.basicConfig(
64
- force=True,
65
- level=level,
66
- format="%(levelprefix)s %(message)s",
67
- handlers=[handler],
68
- )
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
+ })
69
109
 
70
110
 
71
111
  def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
@@ -95,10 +135,6 @@ def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
95
135
  help="Set the logging level."
96
136
  )
97
137
 
98
- features = parser.add_argument_group(title="API features")
99
- features.add_argument("--enable-docs", action="store_true", help="enable the 'docs' endpoint.")
100
- features.add_argument("--enable-write", action="store_true", help="enable support for write operations.")
101
-
102
138
  driver = parser.add_argument_group("database type")
103
139
  db_type = driver.add_mutually_exclusive_group(required=True)
104
140
  db_type.add_argument("--sqlite", action="store_const", dest="db_driver", const="sqlite+aiosqlite", help="use a SQLite database driver.")
auto_rest/handlers.py CHANGED
@@ -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(__name__)
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
- 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_)
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
- query = apply_pagination_params(query, _limit_, _offset_)
217
- query = apply_ordering_params(query, _order_by_, _direction_)
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()]
auto_rest/models.py CHANGED
@@ -34,6 +34,7 @@ import logging
34
34
  from pathlib import Path
35
35
  from typing import Callable
36
36
 
37
+ import yaml
37
38
  from sqlalchemy import create_engine, Engine, MetaData, URL
38
39
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
39
40
  from sqlalchemy.orm import Session
@@ -45,15 +46,34 @@ __all__ = [
45
46
  "create_db_metadata",
46
47
  "create_db_url",
47
48
  "create_session_iterator",
49
+ "parse_db_settings"
48
50
  ]
49
51
 
50
- logger = logging.getLogger(__name__)
52
+ logger = logging.getLogger("auto_rest")
51
53
 
52
54
  # Base classes and typing objects.
53
55
  DBEngine = Engine | AsyncEngine
54
56
  DBSession = Session | AsyncSession
55
57
 
56
58
 
59
+ def parse_db_settings(path: Path | None) -> dict[str, any]:
60
+ """Parse engine configuration settings from a given file path.
61
+
62
+ Args:
63
+ path: Path to the configuration file.
64
+
65
+ Returns:
66
+ Engine configuration settings.
67
+ """
68
+
69
+ if path is not None:
70
+ logger.debug(f"Parsing engine configuration from {path}.")
71
+ return yaml.safe_load(path.read_text()) or dict()
72
+
73
+ logger.debug("No configuration file specified.")
74
+ return {}
75
+
76
+
57
77
  def create_db_url(
58
78
  driver: str,
59
79
  database: str,
@@ -76,21 +96,23 @@ def create_db_url(
76
96
  A fully qualified database URL.
77
97
  """
78
98
 
79
- logger.debug("Resolving database URL.")
80
-
81
99
  # Handle special case where SQLite uses file paths.
82
100
  if "sqlite" in driver:
83
101
  path = Path(database).resolve()
84
- return URL.create(drivername=driver, database=str(path))
102
+ url = URL.create(drivername=driver, database=str(path))
85
103
 
86
- return URL.create(
87
- drivername=driver,
88
- username=username,
89
- password=password,
90
- host=host,
91
- port=port,
92
- database=database,
93
- )
104
+ else:
105
+ url = URL.create(
106
+ drivername=driver,
107
+ username=username,
108
+ password=password,
109
+ host=host,
110
+ port=port,
111
+ database=database,
112
+ )
113
+
114
+ logger.debug(f"Resolved URL: {url}")
115
+ return url
94
116
 
95
117
 
96
118
  def create_db_engine(url: URL, **kwargs: dict[str: any]) -> DBEngine:
@@ -107,8 +129,6 @@ def create_db_engine(url: URL, **kwargs: dict[str: any]) -> DBEngine:
107
129
  A SQLAlchemy `Engine` or `AsyncEngine` instance.
108
130
  """
109
131
 
110
- logger.debug(f"Building database engine for {url}.")
111
-
112
132
  if url.get_dialect().is_async:
113
133
  engine = create_async_engine(url, **kwargs)
114
134
  logger.debug("Asynchronous connection established.")
auto_rest/queries.py CHANGED
@@ -19,81 +19,24 @@ handling and provides a streamlined interface for database interactions.
19
19
  result = await execute_session_query(async_session, query)
20
20
  ```
21
21
  """
22
- from typing import Literal
22
+
23
+ import logging
23
24
 
24
25
  from fastapi import HTTPException
25
- from sqlalchemy import asc, desc, Executable, Result, Select
26
+ from sqlalchemy import Executable, Result
26
27
  from sqlalchemy.ext.asyncio import AsyncSession
27
28
  from starlette import status
28
29
 
29
30
  from auto_rest.models import DBSession
30
31
 
31
32
  __all__ = [
32
- "apply_ordering_params",
33
- "apply_pagination_params",
34
33
  "commit_session",
35
34
  "delete_session_record",
36
35
  "execute_session_query",
37
36
  "get_record_or_404"
38
37
  ]
39
38
 
40
-
41
- def apply_ordering_params(
42
- query: Select,
43
- order_by: str | None = None,
44
- direction: Literal["desc", "asc"] = "asc"
45
- ) -> Select:
46
- """Apply ordering to a database query.
47
-
48
- Returns a copy of the provided query with ordering parameters applied.
49
-
50
- Args:
51
- query: The database query to apply parameters to.
52
- order_by: The name of the column to order by.
53
- direction: The direction to order by (defaults to "asc").
54
-
55
- Returns:
56
- A copy of the query modified to return ordered values.
57
- """
58
-
59
- if order_by is None:
60
- return query
61
-
62
- if order_by not in query.columns:
63
- raise ValueError(f"Invalid column name: {order_by}")
64
-
65
- # Default to ascending order for an invalid ordering direction
66
- if direction == "desc":
67
- return query.order_by(desc(order_by))
68
-
69
- elif direction == "asc":
70
- return query.order_by(asc(order_by))
71
-
72
- raise ValueError(f"Invalid direction, use 'asc' or 'desc': {direction}")
73
-
74
-
75
- def apply_pagination_params(query: Select, limit: int = 0, offset: int = 0) -> Select:
76
- """Apply pagination to a database query.
77
-
78
- Returns a copy of the provided query with offset and limit parameters applied.
79
-
80
- Args:
81
- query: The database query to apply parameters to.
82
- limit: The number of results to return.
83
- offset: The offset to start with.
84
-
85
- Returns:
86
- A copy of the query modified to only return the paginated values.
87
- """
88
-
89
- if offset < 0 or limit < 0:
90
- raise ValueError("Pagination parameters cannot be negative")
91
-
92
- # Do not apply pagination if not requested
93
- if limit == 0:
94
- return query
95
-
96
- return query.offset(offset or 0).limit(limit)
39
+ logger = logging.getLogger("auto_rest.query")
97
40
 
98
41
 
99
42
  async def commit_session(session: DBSession) -> None:
@@ -123,6 +66,7 @@ async def delete_session_record(session: DBSession, record: Result) -> None:
123
66
  record: The record to be deleted.
124
67
  """
125
68
 
69
+ logger.debug("Deleting record.")
126
70
  if isinstance(session, AsyncSession):
127
71
  await session.delete(record)
128
72
 
@@ -143,6 +87,7 @@ async def execute_session_query(session: DBSession, query: Executable) -> Result
143
87
  The result of the executed query.
144
88
  """
145
89
 
90
+ logger.debug(str(query).replace("\n", " "))
146
91
  if isinstance(session, AsyncSession):
147
92
  return await session.execute(query)
148
93
 
auto_rest/routers.py CHANGED
@@ -23,6 +23,8 @@ routers to be added directly to an API application instance.
23
23
  ```
24
24
  """
25
25
 
26
+ import logging
27
+
26
28
  from fastapi import APIRouter
27
29
  from sqlalchemy import MetaData, Table
28
30
  from starlette import status
@@ -36,6 +38,8 @@ __all__ = [
36
38
  "create_welcome_router",
37
39
  ]
38
40
 
41
+ logger = logging.getLogger("auto_rest")
42
+
39
43
 
40
44
  def create_welcome_router() -> APIRouter:
41
45
  """Create an API router for returning a welcome message.
@@ -44,6 +48,8 @@ def create_welcome_router() -> APIRouter:
44
48
  An `APIRouter` with a single route for retrieving a welcome message.
45
49
  """
46
50
 
51
+ logger.debug("Creating welcome endpoint.")
52
+
47
53
  router = APIRouter()
48
54
  router.add_api_route(
49
55
  path="/",
@@ -71,11 +77,13 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
71
77
  An `APIRouter` with a routes for retrieving application metadata.
72
78
  """
73
79
 
80
+ logger.debug("Creating metadata endpoints.")
81
+
74
82
  router = APIRouter()
75
83
  tags = ["Application Metadata"]
76
84
 
77
85
  router.add_api_route(
78
- path="/app",
86
+ path="/app/",
79
87
  methods=["GET"],
80
88
  endpoint=create_about_handler(name, version),
81
89
  summary="Fetch application metadata.",
@@ -83,7 +91,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
83
91
  )
84
92
 
85
93
  router.add_api_route(
86
- path="/engine",
94
+ path="/engine/",
87
95
  methods=["GET"],
88
96
  endpoint=create_engine_handler(engine),
89
97
  summary="Fetch database metadata.",
@@ -91,7 +99,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
91
99
  )
92
100
 
93
101
  router.add_api_route(
94
- path="/schema",
102
+ path="/schema/",
95
103
  methods=["GET"],
96
104
  endpoint=create_schema_handler(metadata),
97
105
  summary="Fetch the database schema.",
@@ -101,25 +109,25 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
101
109
  return router
102
110
 
103
111
 
104
- def create_table_router(engine: DBEngine, table: Table, writeable: bool = False) -> APIRouter:
112
+ def create_table_router(engine: DBEngine, table: Table) -> APIRouter:
105
113
  """Create an API router with endpoint handlers for a given database table.
106
114
 
107
115
  Args:
108
116
  engine: The SQLAlchemy engine connected to the database.
109
117
  table: The database table to create API endpoints for.
110
- writeable: Whether the router should include support for write operations.
111
118
 
112
119
  Returns:
113
120
  An APIRouter instance with routes for database operations on the table.
114
121
  """
115
122
 
123
+ logger.debug(f"Creating endpoints for table `{table.name}`.")
116
124
  router = APIRouter()
117
125
 
118
126
  # Construct path parameters from primary key columns
119
127
  pk_columns = sorted(column.name for column in table.primary_key.columns)
120
128
  path_params_url = "/".join(f"{{{col_name}}}" for col_name in pk_columns)
121
129
 
122
- # Add route for read operations against the table
130
+ # Add routes for operations against the table
123
131
  router.add_api_route(
124
132
  path="/",
125
133
  methods=["GET"],
@@ -129,16 +137,14 @@ def create_table_router(engine: DBEngine, table: Table, writeable: bool = False)
129
137
  tags=[table.name],
130
138
  )
131
139
 
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
- )
140
+ router.add_api_route(
141
+ path="/",
142
+ methods=["POST"],
143
+ endpoint=create_post_record_handler(engine, table),
144
+ status_code=status.HTTP_201_CREATED,
145
+ summary="Create a new record.",
146
+ tags=[table.name],
147
+ )
142
148
 
143
149
  # Add route for read operations against individual records
144
150
  if pk_columns:
@@ -151,8 +157,6 @@ def create_table_router(engine: DBEngine, table: Table, writeable: bool = False)
151
157
  tags=[table.name],
152
158
  )
153
159
 
154
- # Add routes for write operations against individual records
155
- if pk_columns and writeable:
156
160
  router.add_api_route(
157
161
  path=f"/{path_params_url}/",
158
162
  methods=["PUT"],
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auto-rest-api
3
- Version: 0.1.3
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)
@@ -34,7 +36,7 @@ Description-Content-Type: text/markdown
34
36
 
35
37
  # Auto-REST
36
38
 
37
- A command line tool for deploying dynamically generated REST APIs against relational databases.
39
+ A light-weight CLI tool for deploying dynamically generated REST APIs against relational databases.
38
40
  See the [project documentation](https://better-hpc.github.io/auto-rest/) for detailed usage instructions.
39
41
 
40
42
  ## Supported Databases
@@ -70,10 +72,6 @@ Launch an API by providing connection arguments to a database of your choice.
70
72
 
71
73
  ```shell
72
74
  auto-rest \
73
- # Enable optional endpoints / functionality
74
- --enable-docs \
75
- --enable-write \
76
- # Define the database type and connection arguments
77
75
  --psql
78
76
  --db-host localhost
79
77
  --db-port 5432
@@ -0,0 +1,14 @@
1
+ auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
2
+ auto_rest/__main__.py,sha256=VkLZ9j1Q9I0isYvO0Bj1HHCQ9Ew_r5O7a0Dn4fUi5Lo,2233
3
+ auto_rest/app.py,sha256=AnBb2jt0yMxTLj_ICWB1mXLgu2pchjlapfdIk2CwNsQ,3128
4
+ auto_rest/cli.py,sha256=3emee1GrUkQZ-DVAboy5O65jPE0vwmdcy-wfaxh2kBs,6239
5
+ auto_rest/handlers.py,sha256=gwMiE5TKA65nrMizQ472A5iCNmYK7B7vlFDWL0Vrphw,12943
6
+ auto_rest/interfaces.py,sha256=WB_0eMDjGF8DpnDN9INHqo7u4x3aklvzAYK4t3JwC7s,3779
7
+ auto_rest/models.py,sha256=pi97l2PsDqjhW52tAIAZKx6bl2QvC-q5kM5OsVP3V_M,5887
8
+ auto_rest/queries.py,sha256=2e6f_S4n1xvaSY8rYJG-koDmqiXHreOIRUteRuqZlv8,3058
9
+ auto_rest/routers.py,sha256=SZoOugauKdCk2Kqzzp9o6tGENusAsP1-YP-6Aam53tc,5636
10
+ auto_rest_api-0.1.5.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
11
+ auto_rest_api-0.1.5.dist-info/METADATA,sha256=cgh43Yv81K8gaf9B-mAAiWX72VnJAGGuQHWa7Qi5-K4,2914
12
+ auto_rest_api-0.1.5.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
13
+ auto_rest_api-0.1.5.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
14
+ auto_rest_api-0.1.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.0.1
2
+ Generator: poetry-core 2.0.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,14 +0,0 @@
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=Z8DMv3KRYLBXlUKoE4e-2qE9YvG-IPgS57C0RpUDWv4,12603
6
- auto_rest/interfaces.py,sha256=WB_0eMDjGF8DpnDN9INHqo7u4x3aklvzAYK4t3JwC7s,3779
7
- auto_rest/models.py,sha256=HCJUQPBmkkfVFv9CRcPzMP66pQk_UIb3j3cdwHqodwE,5388
8
- auto_rest/queries.py,sha256=Z2ATkcSYldV6BkcrmLogmBoDkPEkyFzFqI7qPcq86uc,4705
9
- auto_rest/routers.py,sha256=RqBwLqVFU1OIFSCUuwOtTis8mT_zyWTaB_8SyIXjfe0,5727
10
- auto_rest_api-0.1.3.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
11
- auto_rest_api-0.1.3.dist-info/METADATA,sha256=Cs7u-2jZGTTlvBprPzdLVWl1uSAAjjLgeh5PuHo3tUk,2951
12
- auto_rest_api-0.1.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
13
- auto_rest_api-0.1.3.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
14
- auto_rest_api-0.1.3.dist-info/RECORD,,