auto-rest-api 0.1.3__py3-none-any.whl → 0.1.4__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,42 @@ 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
+
17
19
  import uvicorn
18
- from fastapi import FastAPI
20
+ from fastapi import FastAPI, Request
19
21
  from fastapi.middleware.cors import CORSMiddleware
22
+ from starlette.responses import Response
20
23
 
21
24
  __all__ = ["create_app", "run_server"]
22
25
 
26
+ logger = logging.getLogger("auto-rest")
27
+
28
+
29
+ async def logging_middleware(request: Request, call_next: callable) -> Response:
30
+ """FastAPI middleware for the logging response status codes.
31
+
32
+ Args:
33
+ request: The incoming HTTP request.
34
+ call_next: The next middleware in the middleware chain.
35
+
36
+ Returns:
37
+ The outgoing HTTP response.
38
+ """
39
+
40
+ response = await call_next(request)
41
+ level = logging.INFO if response.status_code < 400 else logging.ERROR
42
+ logger.log(level, f"{request.method} ({response.status_code}) {request.client.host} - {request.url.path}")
43
+ return response
23
44
 
24
- def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
45
+
46
+ def create_app(app_title: str, app_version: str) -> FastAPI:
25
47
  """Create and configure a FastAPI application instance.
26
48
 
27
49
  This function initializes a FastAPI app with a customizable title, version,
@@ -31,7 +53,6 @@ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
31
53
  Args:
32
54
  app_title: The title of the FastAPI application.
33
55
  app_version: The version of the FastAPI application.
34
- enable_docs: Whether to enable the `/docs/` endpoint.
35
56
 
36
57
  Returns:
37
58
  FastAPI: A configured FastAPI application instance.
@@ -40,14 +61,15 @@ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
40
61
  app = FastAPI(
41
62
  title=app_title,
42
63
  version=app_version,
43
- docs_url="/docs/" if enable_docs else None,
64
+ docs_url="/docs/",
44
65
  redoc_url=None,
45
66
  )
46
67
 
68
+ app.middleware("http")(logging_middleware)
47
69
  app.add_middleware(
48
70
  CORSMiddleware,
49
- allow_origins=["*"],
50
71
  allow_credentials=True,
72
+ allow_origins=["*"],
51
73
  allow_methods=["*"],
52
74
  allow_headers=["*"],
53
75
  )
@@ -55,7 +77,7 @@ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
55
77
  return app
56
78
 
57
79
 
58
- def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
80
+ def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
59
81
  """Deploy a FastAPI application server.
60
82
 
61
83
  Args:
@@ -64,4 +86,5 @@ def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
64
86
  port: The port number for the server to listen on.
65
87
  """
66
88
 
67
- uvicorn.run(app, host=host, port=port, log_level="error")
89
+ # Uvicorn overwrites its logging level when run and needs to be manually disabled here.
90
+ uvicorn.run(app, host=host, port=port, log_level=1000)
auto_rest/cli.py CHANGED
@@ -62,11 +62,13 @@ def configure_cli_logging(level: str) -> None:
62
62
  handler.setFormatter(DefaultFormatter(fmt="%(levelprefix)s %(message)s"))
63
63
  logging.basicConfig(
64
64
  force=True,
65
- level=level,
66
65
  format="%(levelprefix)s %(message)s",
67
66
  handlers=[handler],
68
67
  )
69
68
 
69
+ logging.getLogger("auto-rest").setLevel(level)
70
+ logging.getLogger("sqlalchemy").setLevel(1000)
71
+
70
72
 
71
73
  def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
72
74
  """Create a command-line argument parser with preconfigured arguments.
@@ -95,10 +97,6 @@ def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
95
97
  help="Set the logging level."
96
98
  )
97
99
 
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
100
  driver = parser.add_argument_group("database type")
103
101
  db_type = driver.add_mutually_exclusive_group(required=True)
104
102
  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
@@ -75,7 +75,7 @@ __all__ = [
75
75
  "create_welcome_handler",
76
76
  ]
77
77
 
78
- logger = logging.getLogger(__name__)
78
+ logger = logging.getLogger("auto-rest")
79
79
 
80
80
 
81
81
  def create_welcome_handler() -> Callable[[], Awaitable[PydanticModel]]:
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 connection 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))
85
-
86
- return URL.create(
87
- drivername=driver,
88
- username=username,
89
- password=password,
90
- host=host,
91
- port=port,
92
- database=database,
93
- )
102
+ url = URL.create(drivername=driver, database=str(path))
103
+
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:
auto_rest/queries.py CHANGED
@@ -19,6 +19,7 @@ handling and provides a streamlined interface for database interactions.
19
19
  result = await execute_session_query(async_session, query)
20
20
  ```
21
21
  """
22
+
22
23
  from typing import Literal
23
24
 
24
25
  from fastapi import HTTPException
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,6 +77,8 @@ 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
 
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auto-rest-api
3
- Version: 0.1.3
3
+ Version: 0.1.4
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
@@ -34,7 +34,7 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  # Auto-REST
36
36
 
37
- A command line tool for deploying dynamically generated REST APIs against relational databases.
37
+ A light-weight CLI tool for deploying dynamically generated REST APIs against relational databases.
38
38
  See the [project documentation](https://better-hpc.github.io/auto-rest/) for detailed usage instructions.
39
39
 
40
40
  ## Supported Databases
@@ -70,10 +70,6 @@ Launch an API by providing connection arguments to a database of your choice.
70
70
 
71
71
  ```shell
72
72
  auto-rest \
73
- # Enable optional endpoints / functionality
74
- --enable-docs \
75
- --enable-write \
76
- # Define the database type and connection arguments
77
73
  --psql
78
74
  --db-host localhost
79
75
  --db-port 5432
@@ -0,0 +1,14 @@
1
+ auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
2
+ auto_rest/__main__.py,sha256=uro9m2IKu1r6AGsBVDEFpn8_1HecU_UElpsVCD5UUa0,2233
3
+ auto_rest/app.py,sha256=va53cdA3v2OIw2uzPNcINgN70HhsuF0SPb-SFgUb8A4,2578
4
+ auto_rest/cli.py,sha256=r51sZxdq1XGSvD1LCwtgnMxycdAYLVdOLlPKfJuL2kI,5007
5
+ auto_rest/handlers.py,sha256=HBEQf5ZQVVHpxe0YZ0JHI499U_vCVKOTkzjMSFyR3pY,12606
6
+ auto_rest/interfaces.py,sha256=WB_0eMDjGF8DpnDN9INHqo7u4x3aklvzAYK4t3JwC7s,3779
7
+ auto_rest/models.py,sha256=AE9Ms1YtkVFuT7mTeOGJc3unez630eHbFLFLtwNttpE,5942
8
+ auto_rest/queries.py,sha256=SoQdgsmWo_KuxFD2ibDFAS-fJduV48CnbJrtuxse8Js,4706
9
+ auto_rest/routers.py,sha256=01Zhaynh8ZNjShrNqiyJRjqIx22WhiFmC_OosJHvM-k,5633
10
+ auto_rest_api-0.1.4.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
11
+ auto_rest_api-0.1.4.dist-info/METADATA,sha256=A3N088UrR3WL6AsKCeiorVetAA9X08kPKUQJKgJkvAI,2818
12
+ auto_rest_api-0.1.4.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
13
+ auto_rest_api-0.1.4.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
14
+ auto_rest_api-0.1.4.dist-info/RECORD,,
@@ -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,,