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 +3 -0
- auto_rest/__main__.py +93 -0
- auto_rest/cli.py +127 -0
- auto_rest/handlers.py +362 -0
- auto_rest/models.py +228 -0
- auto_rest/params.py +147 -0
- auto_rest/queries.py +107 -0
- auto_rest/routers.py +168 -0
- auto_rest_api-0.1.0.dist-info/LICENSE.md +675 -0
- auto_rest_api-0.1.0.dist-info/METADATA +86 -0
- auto_rest_api-0.1.0.dist-info/RECORD +13 -0
- auto_rest_api-0.1.0.dist-info/WHEEL +4 -0
- auto_rest_api-0.1.0.dist-info/entry_points.txt +3 -0
auto_rest/models.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `models` module facilitates communication with relational databases
|
|
3
|
+
via dynamically generated object relational mappers (ORMs). Building
|
|
4
|
+
on the popular SQLAlchemy package, it natively supports multiple
|
|
5
|
+
Database Management Systems (DBMS) without requiring custom configuration
|
|
6
|
+
or setup.
|
|
7
|
+
|
|
8
|
+
!!! example "Example: Creating ORM objects"
|
|
9
|
+
|
|
10
|
+
Utility functions are provided for connecting to the database,
|
|
11
|
+
mapping the schema, and dynamically generating ORM models based on
|
|
12
|
+
the existing database structure.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
connection_args = dict(...)
|
|
16
|
+
db_url = create_db_url(**connection_args)
|
|
17
|
+
db_conn = create_db_engine(db_url)
|
|
18
|
+
db_meta = create_db_metadata(db_conn)
|
|
19
|
+
db_models = create_db_models(db_meta)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Support for asynchronous operations is automatically determined based on
|
|
23
|
+
the chosen database. If the driver supports asynchronous operations, the
|
|
24
|
+
connection and session handling are configured accordingly.
|
|
25
|
+
|
|
26
|
+
!!! important "Developer Note"
|
|
27
|
+
|
|
28
|
+
When working with database objects, the returned object type may vary
|
|
29
|
+
depending on whether the underlying driver is synchronous or asynchronous.
|
|
30
|
+
Of particular note are database engines (`Engine` / `AsyncEngine`) and
|
|
31
|
+
sessions (`Session` / `AsyncSession`).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import asyncio
|
|
35
|
+
import logging
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Callable
|
|
38
|
+
|
|
39
|
+
from pydantic.main import create_model, ModelT
|
|
40
|
+
from sqlalchemy import create_engine, Engine, MetaData, URL
|
|
41
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
|
42
|
+
from sqlalchemy.orm import declarative_base, Session
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"DBEngine",
|
|
46
|
+
"DBModel",
|
|
47
|
+
"DBSession",
|
|
48
|
+
"create_db_engine",
|
|
49
|
+
"create_db_interface",
|
|
50
|
+
"create_db_metadata",
|
|
51
|
+
"create_db_models",
|
|
52
|
+
"create_db_url",
|
|
53
|
+
"create_session_iterator",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
# Base classes and typing objects.
|
|
59
|
+
DBModel = declarative_base()
|
|
60
|
+
DBEngine = Engine | AsyncEngine
|
|
61
|
+
DBSession = Session | AsyncSession
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_db_url(
|
|
65
|
+
driver: str,
|
|
66
|
+
database: str,
|
|
67
|
+
host: str | None = None,
|
|
68
|
+
port: int | None = None,
|
|
69
|
+
username: str | None = None,
|
|
70
|
+
password: str | None = None,
|
|
71
|
+
) -> URL:
|
|
72
|
+
"""Create a database URL from the provided parameters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
driver: The SQLAlchemy-compatible database driver.
|
|
76
|
+
database: The database name or file path (for SQLite).
|
|
77
|
+
host: The database server hostname or IP address.
|
|
78
|
+
port: The database server port number.
|
|
79
|
+
username: The username for authentication.
|
|
80
|
+
password: The password for the database user.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A fully qualified database URL.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
logger.debug("Resolving database URL.")
|
|
87
|
+
|
|
88
|
+
# Handle special case where SQLite uses file paths.
|
|
89
|
+
if "sqlite" in driver:
|
|
90
|
+
path = Path(database).resolve()
|
|
91
|
+
return URL.create(drivername=driver, database=str(path))
|
|
92
|
+
|
|
93
|
+
return URL.create(
|
|
94
|
+
drivername=driver,
|
|
95
|
+
username=username,
|
|
96
|
+
password=password,
|
|
97
|
+
host=host,
|
|
98
|
+
port=port,
|
|
99
|
+
database=database,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_db_engine(url: URL, **kwargs: dict[str: any]) -> DBEngine:
|
|
104
|
+
"""Initialize a new database engine.
|
|
105
|
+
|
|
106
|
+
Instantiates and returns an `Engine` or `AsyncEngine` instance depending
|
|
107
|
+
on whether the database URL uses a driver with support for async operations.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
url: A fully qualified database URL.
|
|
111
|
+
**kwargs: Keyword arguments passed to `create_engine`.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
A SQLAlchemy `Engine` or `AsyncEngine` instance.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
logger.debug(f"Building database engine for {url}.")
|
|
118
|
+
|
|
119
|
+
if url.get_dialect().is_async:
|
|
120
|
+
engine = create_async_engine(url, **kwargs)
|
|
121
|
+
logger.debug("Asynchronous connection established.")
|
|
122
|
+
return engine
|
|
123
|
+
|
|
124
|
+
else:
|
|
125
|
+
engine = create_engine(url, **kwargs)
|
|
126
|
+
logger.debug("Synchronous connection established.")
|
|
127
|
+
return engine
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def _async_reflect_metadata(engine: AsyncEngine, metadata: MetaData) -> None:
|
|
131
|
+
"""Helper function used to reflect database metadata using an async engine."""
|
|
132
|
+
|
|
133
|
+
async with engine.connect() as connection:
|
|
134
|
+
await connection.run_sync(metadata.reflect)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def create_db_metadata(engine: DBEngine) -> MetaData:
|
|
138
|
+
"""Create and reflect metadata for the database connection.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
engine: The database engine to use for reflection.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
A MetaData object reflecting the database schema.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
logger.debug("Loading database metadata.")
|
|
148
|
+
metadata = MetaData()
|
|
149
|
+
|
|
150
|
+
if isinstance(engine, AsyncEngine):
|
|
151
|
+
asyncio.run(_async_reflect_metadata(engine, metadata))
|
|
152
|
+
|
|
153
|
+
else:
|
|
154
|
+
metadata.reflect(bind=engine)
|
|
155
|
+
|
|
156
|
+
return metadata
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_db_models(metadata: MetaData) -> dict[str, DBModel]:
|
|
160
|
+
"""Dynamically generate database models from a metadata instance.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
metadata: A reflection of database metadata.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A dictionary mapping table names to database models.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
logger.debug("Building database models...")
|
|
170
|
+
models = {}
|
|
171
|
+
|
|
172
|
+
# Dynamically create a class for each table.
|
|
173
|
+
for table_name, table in metadata.tables.items():
|
|
174
|
+
logger.debug(f"> Creating model for table {table_name}.")
|
|
175
|
+
models[table_name] = type(
|
|
176
|
+
table_name.capitalize(),
|
|
177
|
+
(DBModel,),
|
|
178
|
+
{"__table__": table},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
logger.debug(f"Successfully generated {len(models)} models.")
|
|
182
|
+
return models
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def create_db_interface(model: DBModel) -> type[ModelT]:
|
|
186
|
+
"""Create a Pydantic interface for a SQLAlchemy model.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
model: A SQLAlchemy model to create an interface for.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A Pydantic model class with the same structure as the provided SQLAlchemy model.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
fields = {
|
|
196
|
+
col.name: (col.type.python_type, col.default if col.default is not None else ...)
|
|
197
|
+
for col in model.__table__.columns
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return create_model(model.__name__, **fields)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_session_iterator(engine: DBEngine) -> Callable[[], DBSession]:
|
|
204
|
+
"""Create a generator for database sessions.
|
|
205
|
+
|
|
206
|
+
Returns a synchronous or asynchronous function depending on whether
|
|
207
|
+
the database engine supports async operations. The type of session
|
|
208
|
+
returned also depends on the underlying database engine, and will
|
|
209
|
+
either be a `Session` or `AsyncSession` instance.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
engine: Database engine to use when generating new sessions.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A function that yields a single new database session.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
if isinstance(engine, AsyncEngine):
|
|
219
|
+
async def session_iterator() -> AsyncSession:
|
|
220
|
+
async with AsyncSession(bind=engine, autocommit=False, autoflush=True) as session:
|
|
221
|
+
yield session
|
|
222
|
+
|
|
223
|
+
else:
|
|
224
|
+
def session_iterator() -> Session:
|
|
225
|
+
with Session(bind=engine, autocommit=False, autoflush=True) as session:
|
|
226
|
+
yield session
|
|
227
|
+
|
|
228
|
+
return session_iterator
|
auto_rest/params.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `params` module provides utilities for extracting and applying query
|
|
3
|
+
parameters from incoming HTTP requests. These utilities ensure the consistent
|
|
4
|
+
parsing, validation, and application of query parameters, and automatically
|
|
5
|
+
update HTTP response headers to reflect applied query options.
|
|
6
|
+
|
|
7
|
+
Parameter functions are designed in pairs. A *get* function is used to parse
|
|
8
|
+
parameters from a FastAPI request and an *apply* function is used to apply
|
|
9
|
+
the arguments onto a SQLAlchemy query.
|
|
10
|
+
|
|
11
|
+
!!! example "Example: Parameter Parsing and Application"
|
|
12
|
+
|
|
13
|
+
When handling URL parameters, a *get* function is injected as a dependency
|
|
14
|
+
into the signature of the request handler. The parsed parameter dictionary
|
|
15
|
+
is then passed to an *apply* function.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from fastapi import FastAPI, Response, Depends
|
|
19
|
+
from sqlalchemy import select
|
|
20
|
+
from auto_rest.query_params import get_pagination_params, apply_pagination_params
|
|
21
|
+
|
|
22
|
+
app = FastAPI()
|
|
23
|
+
|
|
24
|
+
@app.get("/items/")
|
|
25
|
+
async def list_items(
|
|
26
|
+
pagination_params: dict = Depends(get_pagination_params),
|
|
27
|
+
response: Response
|
|
28
|
+
):
|
|
29
|
+
query = select(SomeModel)
|
|
30
|
+
query = apply_pagination_params(query, pagination_params, response)
|
|
31
|
+
return ... # Logic to further process and execute the query goes here
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from typing import Literal
|
|
36
|
+
|
|
37
|
+
from fastapi import Query
|
|
38
|
+
from sqlalchemy import asc, desc
|
|
39
|
+
from sqlalchemy.sql.selectable import Select
|
|
40
|
+
from starlette.responses import Response
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"apply_ordering_params",
|
|
44
|
+
"apply_pagination_params",
|
|
45
|
+
"get_ordering_params",
|
|
46
|
+
"get_pagination_params",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_pagination_params(
|
|
51
|
+
_limit_: int = Query(10, ge=0, description="The maximum number of records to return."),
|
|
52
|
+
_offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
|
|
53
|
+
) -> dict[str, int]:
|
|
54
|
+
"""Extract pagination parameters from request query parameters.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
_limit_: The maximum number of records to return.
|
|
58
|
+
_offset_: The starting index of the returned records.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
dict: A dictionary containing the `limit` and `offset` values.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
return {"limit": _limit_, "offset": _offset_}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def apply_pagination_params(query: Select, params: dict[str, int], response: Response) -> Select:
|
|
68
|
+
"""Apply pagination to a database query.
|
|
69
|
+
|
|
70
|
+
Returns a copy of the provided query with offset and limit parameters applied.
|
|
71
|
+
Compatible with parameters returned by the `get_pagination_params` method.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
query: The database query to apply parameters to.
|
|
75
|
+
params: A dictionary containing parsed URL parameters.
|
|
76
|
+
response: The outgoing HTTP response object.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A copy of the query modified to only return the paginated values.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
limit = params.get("limit", 0)
|
|
83
|
+
offset = params.get("offset", 0)
|
|
84
|
+
|
|
85
|
+
if limit < 0 or offset < 0:
|
|
86
|
+
raise ValueError("Pagination parameters must be greater than or equal to zero.")
|
|
87
|
+
|
|
88
|
+
if limit == 0:
|
|
89
|
+
response.headers["X-Pagination-Applied"] = "false"
|
|
90
|
+
return query
|
|
91
|
+
|
|
92
|
+
response.headers["X-Pagination-Applied"] = "true"
|
|
93
|
+
response.headers["X-Pagination-Limit"] = str(limit)
|
|
94
|
+
response.headers["X-Pagination-Offset"] = str(offset)
|
|
95
|
+
return query.offset(offset).limit(limit)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_ordering_params(
|
|
99
|
+
_order_by_: str | None = Query(None, description="The field name to sort by."),
|
|
100
|
+
_direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
|
|
101
|
+
) -> dict:
|
|
102
|
+
"""Extract ordering parameters from request query parameters.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
_order_by_: The field to order by.
|
|
106
|
+
_direction_: The direction to order by.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
dict: A dictionary containing the `order_by` and `direction` values.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
return {"order_by": _order_by_, "direction": _direction_}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def apply_ordering_params(query: Select, params: dict, response: Response) -> Select:
|
|
116
|
+
"""Apply ordering to a database query.
|
|
117
|
+
|
|
118
|
+
Returns a copy of the provided query with ordering parameters applied.
|
|
119
|
+
Compatible with parameters returned by the `get_ordering_params` method.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
query: The database query to apply parameters to.
|
|
123
|
+
params: A dictionary containing parsed URL parameters.
|
|
124
|
+
response: The outgoing HTTP response object.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A copy of the query modified to return ordered values.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
order_by = params.get("order_by")
|
|
131
|
+
direction = params.get("direction", "asc")
|
|
132
|
+
|
|
133
|
+
if not order_by:
|
|
134
|
+
response.headers["X-Order-Applied"] = "false"
|
|
135
|
+
return query
|
|
136
|
+
|
|
137
|
+
response.headers["X-Order-Applied"] = "true"
|
|
138
|
+
response.headers["X-Order-By"] = order_by
|
|
139
|
+
response.headers["X-Order-Direction"] = direction
|
|
140
|
+
|
|
141
|
+
if direction == "asc":
|
|
142
|
+
return query.order_by(asc(order_by))
|
|
143
|
+
|
|
144
|
+
if direction == "desc":
|
|
145
|
+
return query.order_by(desc(order_by))
|
|
146
|
+
|
|
147
|
+
raise ValueError("Ordering direction must be 'asc' or 'desc'.")
|
auto_rest/queries.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `queries` module provides asynchronous wrapper functions around operations
|
|
3
|
+
involving SQLAlchemy sessions. These utilities automatically account for
|
|
4
|
+
variations in behavior between synchronous and asynchronous session types
|
|
5
|
+
(i.e., `Session` and `AsyncSession` instances). This ensures consistent query
|
|
6
|
+
handling and provides a streamlined interface for database interactions.
|
|
7
|
+
|
|
8
|
+
!!! example "Example: Query Execution"
|
|
9
|
+
|
|
10
|
+
Query utilities seamlessly support synchronous and asynchronous session types.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
query = select(SomeModel).where(SomeModel.id == item_id)
|
|
14
|
+
|
|
15
|
+
with Session(...) as sync_session:
|
|
16
|
+
result = await execute_session_query(sync_session, query)
|
|
17
|
+
|
|
18
|
+
with AsyncSession(...) as async_session:
|
|
19
|
+
result = await execute_session_query(async_session, query)
|
|
20
|
+
```
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from fastapi import HTTPException
|
|
24
|
+
from sqlalchemy import Executable, Result
|
|
25
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
26
|
+
from starlette import status
|
|
27
|
+
|
|
28
|
+
from auto_rest.models import DBSession
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"commit_session",
|
|
32
|
+
"delete_session_record",
|
|
33
|
+
"execute_session_query",
|
|
34
|
+
"get_record_or_404"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def commit_session(session: DBSession) -> None:
|
|
39
|
+
"""Commit a SQLAlchemy session.
|
|
40
|
+
|
|
41
|
+
Supports synchronous and asynchronous sessions.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
session: The session to commit.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
if isinstance(session, AsyncSession):
|
|
48
|
+
await session.commit()
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
session.commit()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def delete_session_record(session: DBSession, record: Result) -> None:
|
|
55
|
+
"""Delete a record from the database using an existing session.
|
|
56
|
+
|
|
57
|
+
Does not automatically commit the session.
|
|
58
|
+
Supports synchronous and asynchronous sessions.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
session: The session to use for deletion.
|
|
62
|
+
record: The record to be deleted.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if isinstance(session, AsyncSession):
|
|
66
|
+
await session.delete(record)
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
session.delete(record)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def execute_session_query(session: DBSession, query: Executable) -> Result:
|
|
73
|
+
"""Execute a query in the given session and return the result.
|
|
74
|
+
|
|
75
|
+
Supports synchronous and asynchronous sessions.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
session: The SQLAlchemy session to use for executing the query.
|
|
79
|
+
query: The query to be executed.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The result of the executed query.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if isinstance(session, AsyncSession):
|
|
86
|
+
return await session.execute(query)
|
|
87
|
+
|
|
88
|
+
return session.execute(query)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_record_or_404(result: Result) -> any:
|
|
92
|
+
"""Retrieve a scalar record from a query result or raise a 404 error.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
result: The query result to extract the scalar record from.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The scalar record if it exists.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
HTTPException: If the record is not found.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
if not (record := result.scalar_one_or_none()):
|
|
105
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Record not found")
|
|
106
|
+
|
|
107
|
+
return record
|
auto_rest/routers.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API routers are responsible for redirecting incoming HTTP requests to the
|
|
3
|
+
appropriate handling logic. Router objects are created using a factory
|
|
4
|
+
pattern, with each router being responsible for a single application
|
|
5
|
+
resource. Each factory returns an `APIRouter` instance preconfigured
|
|
6
|
+
with request handling logic for the relevant resource. This allows
|
|
7
|
+
routers to be added directly to an API application instance.
|
|
8
|
+
|
|
9
|
+
!!! example "Example: Creating and Adding a Router"
|
|
10
|
+
|
|
11
|
+
Care should be taken to avoid path conflicts when adding routers
|
|
12
|
+
to an API application instance. Using a unique `prefix` value
|
|
13
|
+
ensures that each router's endpoints are properly namespaced and
|
|
14
|
+
unique.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from auto_rest.routers import create_welcome_router
|
|
19
|
+
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
welcome_router = create_welcome_router()
|
|
22
|
+
app.include_router(welcome_router, prefix="/welcome")
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from fastapi import APIRouter
|
|
27
|
+
from sqlalchemy import MetaData
|
|
28
|
+
from starlette import status
|
|
29
|
+
|
|
30
|
+
from auto_rest.handlers import *
|
|
31
|
+
from auto_rest.models import DBEngine, DBModel
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"create_meta_router",
|
|
35
|
+
"create_model_router",
|
|
36
|
+
"create_welcome_router",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_welcome_router() -> APIRouter:
|
|
41
|
+
"""Create an API router for returning a welcome message.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
An `APIRouter` with a single route for retrieving a welcome message.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
router = APIRouter()
|
|
48
|
+
router.add_api_route("/", create_welcome_handler(), methods=["GET"], include_in_schema=False)
|
|
49
|
+
return router
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version: str) -> APIRouter:
|
|
53
|
+
"""Create an API router for returning database metadata.
|
|
54
|
+
|
|
55
|
+
Includes routes for retrieving the database driver, database schema,
|
|
56
|
+
and application/schema version.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
engine: The database engine used to facilitate database interactions.
|
|
60
|
+
metadata: The metadata object containing the database schema.
|
|
61
|
+
version: The application name.
|
|
62
|
+
version: The application versionnumber.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
An `APIRouter` with a routes for retrieving application metadata.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
router = APIRouter()
|
|
69
|
+
tags = ["Application Metadata"]
|
|
70
|
+
|
|
71
|
+
router.add_api_route("/app", create_about_handler(name, version), methods=["GET"], tags=tags)
|
|
72
|
+
router.add_api_route("/engine", create_engine_handler(engine), methods=["GET"], tags=tags)
|
|
73
|
+
router.add_api_route("/schema", create_schema_handler(metadata), methods=["GET"], tags=tags)
|
|
74
|
+
return router
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def create_model_router(engine: DBEngine, model: DBModel, writeable: bool = False) -> APIRouter:
|
|
78
|
+
"""Create an API router with endpoint handlers for the given database model.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
engine: The SQLAlchemy engine connected to the database.
|
|
82
|
+
model: The ORM model class representing a database table.
|
|
83
|
+
writeable: Whether the router should include support for write operations.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
An APIRouter instance with routes for database operations on the model.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
router = APIRouter()
|
|
90
|
+
|
|
91
|
+
# Construct path parameters from primary key columns
|
|
92
|
+
pk_columns = model.__table__.primary_key.columns
|
|
93
|
+
path_params_url = "/".join(f"{{{col.name}}}" for col in pk_columns)
|
|
94
|
+
path_params_openapi = {
|
|
95
|
+
"parameters": [
|
|
96
|
+
{
|
|
97
|
+
"name": col.name,
|
|
98
|
+
"in": "path",
|
|
99
|
+
"required": True
|
|
100
|
+
} for col in pk_columns
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Raise an error if no primary key columns are found
|
|
105
|
+
# (SQLAlchemy should ensure this never happens)
|
|
106
|
+
if not pk_columns: # pragma: no cover
|
|
107
|
+
raise RuntimeError(f"No primary key columns found for table {model.__tablename__}.")
|
|
108
|
+
|
|
109
|
+
# Define routes for read operations
|
|
110
|
+
router.add_api_route(
|
|
111
|
+
path="/",
|
|
112
|
+
methods=["GET"],
|
|
113
|
+
endpoint=create_list_records_handler(engine, model),
|
|
114
|
+
status_code=status.HTTP_200_OK,
|
|
115
|
+
tags=[model.__name__],
|
|
116
|
+
openapi_extra=path_params_openapi
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
router.add_api_route(
|
|
120
|
+
path=f"/{path_params_url}/",
|
|
121
|
+
methods=["GET"],
|
|
122
|
+
endpoint=create_get_record_handler(engine, model),
|
|
123
|
+
status_code=status.HTTP_200_OK,
|
|
124
|
+
tags=[model.__name__],
|
|
125
|
+
openapi_extra=path_params_openapi
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not writeable:
|
|
129
|
+
return router
|
|
130
|
+
|
|
131
|
+
# Define routes for write operations
|
|
132
|
+
router.add_api_route(
|
|
133
|
+
path="/",
|
|
134
|
+
methods=["POST"],
|
|
135
|
+
endpoint=create_post_record_handler(engine, model),
|
|
136
|
+
status_code=status.HTTP_201_CREATED,
|
|
137
|
+
tags=[model.__name__],
|
|
138
|
+
openapi_extra=path_params_openapi
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
router.add_api_route(
|
|
142
|
+
path=f"/{path_params_url}/",
|
|
143
|
+
methods=["PUT"],
|
|
144
|
+
endpoint=create_put_record_handler(engine, model),
|
|
145
|
+
status_code=status.HTTP_200_OK,
|
|
146
|
+
tags=[model.__name__],
|
|
147
|
+
openapi_extra=path_params_openapi
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
router.add_api_route(
|
|
151
|
+
path=f"/{path_params_url}/",
|
|
152
|
+
methods=["PATCH"],
|
|
153
|
+
endpoint=create_patch_record_handler(engine, model),
|
|
154
|
+
status_code=status.HTTP_200_OK,
|
|
155
|
+
tags=[model.__name__],
|
|
156
|
+
openapi_extra=path_params_openapi
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
router.add_api_route(
|
|
160
|
+
path=f"/{path_params_url}/",
|
|
161
|
+
methods=["DELETE"],
|
|
162
|
+
endpoint=create_delete_record_handler(engine, model),
|
|
163
|
+
status_code=status.HTTP_200_OK,
|
|
164
|
+
tags=[model.__name__],
|
|
165
|
+
openapi_extra=path_params_openapi
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return router
|