auto-rest-api 0.1.0__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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .__main__ import run_application
2
+
3
+ __all__ = ["run_application"]
auto_rest/__main__.py ADDED
@@ -0,0 +1,93 @@
1
+ """Application entrypoint triggered by calling the packaged CLI command."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import uvicorn
7
+ import yaml
8
+ from fastapi import FastAPI
9
+
10
+ from .cli import *
11
+ from .models import *
12
+ from .routers import *
13
+
14
+ __all__ = ["main", "run_application"]
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def main() -> None: # pragma: no cover
20
+ """Parse command-line arguments and launch an API server."""
21
+
22
+ try:
23
+ parser = create_cli_parser()
24
+ args = vars(parser.parse_args())
25
+ log_level = args.pop("log_level")
26
+
27
+ configure_cli_logging(log_level)
28
+ run_application(**args)
29
+
30
+ except KeyboardInterrupt:
31
+ pass
32
+
33
+ except Exception as e:
34
+ logger.critical(str(e), exc_info=True)
35
+
36
+
37
+ def run_application(
38
+ enable_docs: bool,
39
+ enable_write: bool,
40
+ db_driver: str,
41
+ db_host: str,
42
+ db_port: int,
43
+ db_name: str,
44
+ db_user: str,
45
+ db_pass: str,
46
+ db_config: Path | None,
47
+ server_host: str,
48
+ server_port: int,
49
+ app_title: str,
50
+ app_version: str,
51
+ ) -> None: # pragma: no cover
52
+ """Run an Auto-REST API server.
53
+
54
+ This function is equivalent to launching an API server from the command line
55
+ and accepts the same arguments as those provided in the CLI.
56
+
57
+ Args:
58
+ enable_docs: Whether to enable the 'docs' API endpoint.
59
+ enable_write: Whether to enable support for write operations.
60
+ db_driver: SQLAlchemy-compatible database driver.
61
+ db_host: Database host address.
62
+ db_port: Database port number.
63
+ db_name: Database name.
64
+ db_user: Database authentication username.
65
+ db_pass: Database authentication password.
66
+ db_config: Path to a database configuration file.
67
+ server_host: API server host address.
68
+ server_port: API server port number.
69
+ app_title: title for the generated OpenAPI schema.
70
+ app_version: version number for the generated OpenAPI schema.
71
+ """
72
+
73
+ # Connect to and map the database.
74
+ logger.info(f"Mapping database schema for {db_name}.")
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
+ db_conn = create_db_engine(db_url, **db_kwargs)
78
+ db_meta = create_db_metadata(db_conn)
79
+ db_models = create_db_models(db_meta)
80
+
81
+ # Build an empty application and dynamically add the requested functionality.
82
+ logger.info("Creating API application.")
83
+ app = FastAPI(title=app_title, version=app_version, docs_url="/docs/" if enable_docs else None, redoc_url=None)
84
+ app.include_router(create_welcome_router(), prefix="")
85
+ app.include_router(create_meta_router(db_conn, db_meta, app_title, app_version), prefix="/meta")
86
+
87
+ for model_name, model in db_models.items():
88
+ logger.info(f"Adding `/db/{model_name}` endpoint.")
89
+ app.include_router(create_model_router(db_conn, model, enable_write), prefix=f"/db/{model_name}")
90
+
91
+ # Launch the API server.
92
+ logger.info(f"Launching API server on http://{server_host}:{server_port}.")
93
+ uvicorn.run(app, host=server_host, port=server_port, log_level="error")
auto_rest/cli.py ADDED
@@ -0,0 +1,127 @@
1
+ """
2
+ The `cli` module manages input/output operations for the application's
3
+ command line interface (CLI). Application inputs are parsed using the
4
+ built-in `argparse` module while output messages are handled using the
5
+ Python `logging` library.
6
+
7
+ !!! example "Example: Parsing Arguments"
8
+
9
+ The `create_argument_parser` function returns an `ArgumentParser`
10
+ instance with pre-populated argument definitions.
11
+
12
+ ```python
13
+ from auto_rest.cli import create_argument_parser
14
+
15
+ parser = create_argument_parser()
16
+ args = parser.parse_args()
17
+ print(vars(args))
18
+ ```
19
+
20
+ !!! example "Example: Enabling Console Logging"
21
+
22
+ The `configure_cli_logging` method overrides any existing logging
23
+ configurations and enables console logging according to the provided log
24
+ level.
25
+
26
+ ```python
27
+ from auto_rest.cli import configure_cli_logging
28
+
29
+ configure_cli_logging(log_level="INFO")
30
+ ```
31
+ """
32
+
33
+ import importlib.metadata
34
+ import logging
35
+ from argparse import ArgumentParser, HelpFormatter
36
+ from pathlib import Path
37
+
38
+ from uvicorn.logging import DefaultFormatter
39
+
40
+ __all__ = ["VERSION", "configure_cli_logging", "create_cli_parser"]
41
+
42
+ VERSION = importlib.metadata.version("auto-rest-api")
43
+
44
+
45
+ def configure_cli_logging(level: str) -> None:
46
+ """Enable console logging with the specified application log level.
47
+
48
+ Calling this method overrides and removes all previously configured
49
+ logging configurations.
50
+
51
+ Args:
52
+ level: The Python logging level.
53
+ """
54
+
55
+ # Normalize and validate the logging level.
56
+ level = level.upper()
57
+ if level not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
58
+ raise ValueError(f"Invalid logging level: {level}")
59
+
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
+ )
69
+
70
+
71
+ def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
72
+ """Create a command-line argument parser with preconfigured arguments.
73
+
74
+ Args:
75
+ exit_on_error: Whether to exit the program on a parsing error.
76
+
77
+ Returns:
78
+ An argument parser instance.
79
+ """
80
+
81
+ formatter = lambda prog: HelpFormatter(prog, max_help_position=29)
82
+ parser = ArgumentParser(
83
+ prog="auto-rest",
84
+ description="Automatically map database schemas and deploy per-table REST API endpoints.",
85
+ exit_on_error=exit_on_error,
86
+ formatter_class=formatter
87
+ )
88
+
89
+ parser.add_argument("--version", action="version", version=VERSION)
90
+ parser.add_argument(
91
+ "--log-level",
92
+ default="INFO",
93
+ type=lambda x: x.upper(),
94
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
95
+ help="Set the logging level."
96
+ )
97
+
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
+ driver = parser.add_argument_group("database type")
103
+ db_type = driver.add_mutually_exclusive_group(required=True)
104
+ db_type.add_argument("--sqlite", action="store_const", dest="db_driver", const="sqlite+aiosqlite", help="use a SQLite database driver.")
105
+ db_type.add_argument("--psql", action="store_const", dest="db_driver", const="postgresql+asyncpg", help="use a PostgreSQL database driver.")
106
+ db_type.add_argument("--mysql", action="store_const", dest="db_driver", const="mysql+asyncmy", help="use a MySQL database driver.")
107
+ db_type.add_argument("--oracle", action="store_const", dest="db_driver", const="oracle+oracledb", help="use an Oracle database driver.")
108
+ db_type.add_argument("--mssql", action="store_const", dest="db_driver", const="mssql+aiomysql", help="use a Microsoft database driver.")
109
+ db_type.add_argument("--driver", action="store", dest="db_driver", help="use a custom database driver.")
110
+
111
+ database = parser.add_argument_group("database settings")
112
+ database.add_argument("--db-host", help="database address to connect to.")
113
+ database.add_argument("--db-port", type=int, help="database port to connect to.")
114
+ database.add_argument("--db-name", required=True, help="database name or file path to connect to.")
115
+ database.add_argument("--db-user", help="username to authenticate with.")
116
+ database.add_argument("--db-pass", help="password to authenticate with.")
117
+ database.add_argument("--db-config", action="store", type=Path, help="path to a database configuration file.")
118
+
119
+ server = parser.add_argument_group(title="server settings")
120
+ server.add_argument("--server-host", default="127.0.0.1", help="API server host address.")
121
+ server.add_argument("--server-port", type=int, default=8081, help="API server port number.")
122
+
123
+ schema = parser.add_argument_group(title="application settings")
124
+ schema.add_argument("--app-title", default="Auto-REST", help="application title.")
125
+ schema.add_argument("--app-version", default=VERSION, help="application version number.")
126
+
127
+ return parser
auto_rest/handlers.py ADDED
@@ -0,0 +1,362 @@
1
+ """
2
+ An **endpoint handler** is a function designed to process incoming HTTP
3
+ requests for single API endpoint. In `auto_rest`, handlers are
4
+ created dynamically using a factory pattern. This approach allows
5
+ handler logic to be customized and reused across multiple endpoints.
6
+
7
+ !!! example "Example: Creating a Handler"
8
+
9
+ New endpoint handlers are created dynamically using factory methods.
10
+
11
+ ```python
12
+ welcome_handler = create_welcome_handler()
13
+ ```
14
+
15
+ Handler functions are defined as asynchronous coroutines.
16
+ This provides improved performance when handling large numbers of
17
+ incoming requests.
18
+
19
+ !!! example "Example: Async Handlers"
20
+
21
+ Python requires asynchronous coroutines to be run from an asynchronous
22
+ context. In the following example, this is achieved using `asyncio.run`.
23
+
24
+ ```python
25
+ import asyncio
26
+
27
+ return_value = asyncio.run(welcome_handler())
28
+ ```
29
+
30
+ Handlers are specifically designed to integrate with the FastAPI framework,
31
+ including support for FastAPI's type hinting and data validation capabilities.
32
+ This makes it easy to incorporate handlers into a FastAPI application.
33
+
34
+ !!! example "Example: Adding a Handler to an Application"
35
+
36
+ Use the `add_api_route` method to dynamically add handler functions to
37
+ an existing application instance.
38
+
39
+ ```python
40
+ app = FastAPI(...)
41
+
42
+ handler = create_welcome_handler()
43
+ app.add_api_route("/", handler, methods=["GET"], ...)
44
+ ```
45
+
46
+ !!! info "Developer Note"
47
+
48
+ FastAPI internally performs post-processing on values returned by endpoint
49
+ handlers before sending them in an HTTP response. For this reason, handlers
50
+ should always be tested within the context of a FastAPI application.
51
+ """
52
+
53
+ import logging
54
+ from typing import Awaitable, Callable
55
+
56
+ from fastapi import Depends, Response
57
+ from pydantic import create_model
58
+ from pydantic.main import ModelT
59
+ from sqlalchemy import insert, MetaData, select
60
+ from starlette.requests import Request
61
+
62
+ from .models import *
63
+ from .params import *
64
+ from .queries import *
65
+
66
+ __all__ = [
67
+ "create_about_handler",
68
+ "create_delete_record_handler",
69
+ "create_engine_handler",
70
+ "create_get_record_handler",
71
+ "create_list_records_handler",
72
+ "create_patch_record_handler",
73
+ "create_post_record_handler",
74
+ "create_put_record_handler",
75
+ "create_schema_handler",
76
+ "create_welcome_handler",
77
+ ]
78
+
79
+ logger = logging.getLogger(__name__)
80
+
81
+
82
+ def create_welcome_handler() -> Callable[[], Awaitable[ModelT]]:
83
+ """Create an endpoint handler that returns an application welcome message.
84
+
85
+ Returns:
86
+ An async function that returns a welcome message.
87
+ """
88
+
89
+ interface = create_model("Welcome", message=(str, "Welcome to Auto-Rest!"))
90
+
91
+ async def welcome_handler() -> interface:
92
+ """Return an application welcome message."""
93
+
94
+ return interface()
95
+
96
+ return welcome_handler
97
+
98
+
99
+ def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[ModelT]]:
100
+ """Create an endpoint handler that returns the application name and version number.
101
+
102
+ Args:
103
+ name: The application name.
104
+ version: The returned version identifier.
105
+
106
+ Returns:
107
+ An async function that returns aplication info.
108
+ """
109
+
110
+ interface = create_model("Version", version=(str, version), name=(str, name))
111
+
112
+ async def about_handler() -> interface:
113
+ """Return the application name and version number."""
114
+
115
+ return interface()
116
+
117
+ return about_handler
118
+
119
+
120
+ def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[ModelT]]:
121
+ """Create an endpoint handler that returns configuration details for a database engine.
122
+
123
+ Args:
124
+ engine: Database engine to return the configuration for.
125
+
126
+ Returns:
127
+ An async function that returns database metadata.
128
+ """
129
+
130
+ interface = create_model("Meta",
131
+ dialect=(str, engine.dialect.name),
132
+ driver=(str, engine.dialect.driver),
133
+ database=(str, engine.url.database),
134
+ )
135
+
136
+ async def meta_handler() -> interface:
137
+ """Return metadata concerning the underlying application database."""
138
+
139
+ return interface()
140
+
141
+ return meta_handler
142
+
143
+
144
+ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[ModelT]]:
145
+ """Create an endpoint handler that returns the database schema.
146
+
147
+ Args:
148
+ metadata: Metadata object containing the database schema.
149
+
150
+ Returns:
151
+ An async function that returns the database schema.
152
+ """
153
+
154
+ # Define Pydantic models for column, table, and schema level data
155
+ column_interface = create_model("Column",
156
+ type=(str, ...),
157
+ nullable=(bool, ...),
158
+ default=(str | None, None),
159
+ primary_key=(bool, ...),
160
+ )
161
+
162
+ table_interface = create_model("Table", columns=(dict[str, column_interface], ...))
163
+ schema_interface = create_model("Schema", tables=(dict[str, table_interface], ...))
164
+
165
+ async def schema_handler() -> schema_interface:
166
+ """Return metadata concerning the underlying application database."""
167
+
168
+ return schema_interface(
169
+ tables={table_name: table_interface(columns={
170
+ column.name: column_interface(
171
+ type=str(column.type),
172
+ nullable=column.nullable,
173
+ default=str(column.default.arg) if column.default else None,
174
+ primary_key=column.primary_key
175
+ )
176
+ for column in table.columns
177
+ }) for table_name, table in metadata.tables.items()}
178
+ )
179
+
180
+ return schema_handler
181
+
182
+
183
+ def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[list[ModelT]]]:
184
+ """Create an endpoint handler that returns a list of records from a database table.
185
+
186
+ Args:
187
+ engine: Database engine to use when executing queries.
188
+ model: The ORM object to use for database manipulations.
189
+
190
+ Returns:
191
+ An async function that returns a list of records from the given database model.
192
+ """
193
+
194
+ interface = create_db_interface(model)
195
+
196
+ async def list_records_handler(
197
+ response: Response,
198
+ session: DBSession = Depends(create_session_iterator(engine)),
199
+ pagination_params: dict[str, int] = Depends(get_pagination_params),
200
+ ordering_params: dict[str, int] = Depends(get_ordering_params),
201
+ ) -> list[interface]:
202
+ """Fetch a list of records from the database.
203
+
204
+ URL query parameters are used to enable filtering, ordering, and paginating returned values.
205
+ """
206
+
207
+ query = select(model)
208
+ query = apply_pagination_params(query, pagination_params, response)
209
+ query = apply_ordering_params(query, ordering_params, response)
210
+ result = await execute_session_query(session, query)
211
+ return [interface.model_validate(record.__dict__) for record in result.scalars().all()]
212
+
213
+ return list_records_handler
214
+
215
+
216
+ def create_get_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[ModelT]]:
217
+ """Create a function for handling GET requests against a single record in the database.
218
+
219
+ Args:
220
+ engine: Database engine to use when executing queries.
221
+ model: The ORM object to use for database manipulations.
222
+
223
+ Returns:
224
+ An async function that returns a single record from the given database model.
225
+ """
226
+
227
+ interface = create_db_interface(model)
228
+
229
+ async def get_record_handler(
230
+ request: Request,
231
+ session: DBSession = Depends(create_session_iterator(engine)),
232
+ ) -> interface:
233
+ """Fetch a single record from the database."""
234
+
235
+ query = select(model).filter_by(**request.path_params)
236
+ result = await execute_session_query(session, query)
237
+ record = get_record_or_404(result)
238
+ return interface.model_validate(record.__dict__)
239
+
240
+ return get_record_handler
241
+
242
+
243
+ def create_post_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[ModelT]]:
244
+ """Create a function for handling POST requests against a record in the database.
245
+
246
+ Args:
247
+ engine: Database engine to use when executing queries.
248
+ model: The ORM object to use for database manipulations.
249
+
250
+ Returns:
251
+ An async function that handles record creation.
252
+ """
253
+
254
+ interface = create_db_interface(model)
255
+
256
+ async def post_record_handler(
257
+ data: interface,
258
+ session: DBSession = Depends(create_session_iterator(engine)),
259
+ ) -> interface:
260
+ """Create a new record in the database."""
261
+
262
+ query = insert(model).values(**data.dict())
263
+ result = await execute_session_query(session, query)
264
+ record = get_record_or_404(result)
265
+
266
+ await commit_session(session)
267
+ return interface.model_validate(record.__dict__)
268
+
269
+ return post_record_handler
270
+
271
+
272
+ def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[ModelT]]:
273
+ """Create a function for handling PUT requests against a record in the database.
274
+
275
+ Args:
276
+ engine: Database engine to use when executing queries.
277
+ model: The ORM object to use for database manipulations.
278
+
279
+ Returns:
280
+ An async function that handles record updates.
281
+ """
282
+
283
+ interface = create_db_interface(model)
284
+
285
+ async def put_record_handler(
286
+ request: Request,
287
+ data: interface,
288
+ session: DBSession = Depends(create_session_iterator(engine)),
289
+ ) -> interface:
290
+ """Replace record values in the database with the provided data."""
291
+
292
+ query = select(model).filter_by(**request.path_params)
293
+ result = await execute_session_query(session, query)
294
+ record = get_record_or_404(result)
295
+
296
+ for key, value in data.dict().items():
297
+ setattr(record, key, value)
298
+
299
+ await commit_session(session)
300
+ return interface.model_validate(record.__dict__)
301
+
302
+ return put_record_handler
303
+
304
+
305
+ def create_patch_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[ModelT]]:
306
+ """Create a function for handling PATCH requests against a record in the database.
307
+
308
+ Args:
309
+ engine: Database engine to use when executing queries.
310
+ model: The ORM object to use for database manipulations.
311
+
312
+ Returns:
313
+ An async function that handles record updates.
314
+ """
315
+
316
+ interface = create_db_interface(model)
317
+
318
+ async def patch_record_handler(
319
+ request: Request,
320
+ data: interface,
321
+ session: DBSession = Depends(create_session_iterator(engine)),
322
+ ) -> interface:
323
+ """Update record values in the database with the provided data."""
324
+
325
+ query = select(model).filter_by(**request.path_params)
326
+ result = await execute_session_query(session, query)
327
+ record = get_record_or_404(result)
328
+
329
+ for key, value in data.dict(exclude_unset=True).items():
330
+ setattr(record, key, value)
331
+
332
+ await commit_session(session)
333
+ return interface(record.__dict__)
334
+
335
+ return patch_record_handler
336
+
337
+
338
+ def create_delete_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[None]]:
339
+ """Create a function for handling DELETE requests against a record in the database.
340
+
341
+ Args:
342
+ engine: Database engine to use when executing queries.
343
+ model: The ORM object to use for database manipulations.
344
+
345
+ Returns:
346
+ An async function that handles record deletion.
347
+ """
348
+
349
+ async def delete_record_handler(
350
+ request: Request,
351
+ session: DBSession = Depends(create_session_iterator(engine)),
352
+ ) -> None:
353
+ """Delete a record from the database."""
354
+
355
+ query = select(model).filter_by(**request.path_params)
356
+ result = await execute_session_query(session, query)
357
+ record = get_record_or_404(result)
358
+
359
+ await delete_session_record(session, record)
360
+ await commit_session(session)
361
+
362
+ return delete_record_handler