auto-rest-api 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auto-rest-api
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -69,13 +69,15 @@ def run_application(
69
69
  app_version: version number for the generated OpenAPI schema.
70
70
  """
71
71
 
72
- # Connect to and map the database.
73
72
  logger.info(f"Mapping database schema for {db_name}.")
73
+
74
+ # Resolve database connection settings
74
75
  db_url = create_db_url(driver=db_driver, host=db_host, port=db_port, database=db_name, username=db_user, password=db_pass)
75
76
  db_kwargs = yaml.safe_load(db_config.read_text()) if db_config else {}
77
+
78
+ # Connect to and map the database.
76
79
  db_conn = create_db_engine(db_url, **db_kwargs)
77
80
  db_meta = create_db_metadata(db_conn)
78
- db_models = create_db_models(db_meta)
79
81
 
80
82
  # Build an empty application and dynamically add the requested functionality.
81
83
  logger.info("Creating API application.")
@@ -83,9 +85,9 @@ def run_application(
83
85
  app.include_router(create_welcome_router(), prefix="")
84
86
  app.include_router(create_meta_router(db_conn, db_meta, app_title, app_version), prefix="/meta")
85
87
 
86
- for model_name, model in db_models.items():
87
- logger.info(f"Adding `/db/{model_name}` endpoint.")
88
- app.include_router(create_model_router(db_conn, model, enable_write), prefix=f"/db/{model_name}")
88
+ 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}")
89
91
 
90
92
  # Launch the API server.
91
93
  logger.info(f"Launching API server on http://{server_host}:{server_port}.")
@@ -56,9 +56,9 @@ from typing import Awaitable, Callable
56
56
  from fastapi import Depends, Response
57
57
  from pydantic import create_model
58
58
  from pydantic.main import BaseModel as PydanticModel
59
- from sqlalchemy import insert, MetaData, select
60
- from starlette.requests import Request
59
+ from sqlalchemy import insert, MetaData, select, Table
61
60
 
61
+ from .interfaces import *
62
62
  from .models import *
63
63
  from .params import *
64
64
  from .queries import *
@@ -180,120 +180,121 @@ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[Pydantic
180
180
  return schema_handler
181
181
 
182
182
 
183
- def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[list[PydanticModel]]]:
183
+ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[list[PydanticModel]]]:
184
184
  """Create an endpoint handler that returns a list of records from a database table.
185
185
 
186
186
  Args:
187
187
  engine: Database engine to use when executing queries.
188
- model: The ORM object to use for database manipulations.
188
+ table: The database table to query against.
189
189
 
190
190
  Returns:
191
191
  An async function that returns a list of records from the given database model.
192
192
  """
193
193
 
194
- interface = create_db_interface(model)
194
+ interface = create_interface(table)
195
195
 
196
196
  async def list_records_handler(
197
197
  response: Response,
198
198
  session: DBSession = Depends(create_session_iterator(engine)),
199
- pagination_params: dict[str, int] = create_pagination_dependency(model),
200
- ordering_params: dict[str, int] = create_ordering_dependency(model),
199
+ pagination_params: dict[str, int] = create_pagination_dependency(table),
200
+ ordering_params: dict[str, int] = create_ordering_dependency(table),
201
201
  ) -> list[interface]:
202
202
  """Fetch a list of records from the database.
203
203
 
204
204
  URL query parameters are used to enable filtering, ordering, and paginating returned values.
205
205
  """
206
206
 
207
- query = select(model)
207
+ query = select(table)
208
208
  query = apply_pagination_params(query, pagination_params, response)
209
209
  query = apply_ordering_params(query, ordering_params, response)
210
+
210
211
  result = await execute_session_query(session, query)
211
- return [interface.model_validate(record.__dict__) for record in result.scalars().all()]
212
+ return [row._mapping for row in result.all()]
212
213
 
213
214
  return list_records_handler
214
215
 
215
216
 
216
- def create_get_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
217
+ def create_get_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
217
218
  """Create a function for handling GET requests against a single record in the database.
218
219
 
219
220
  Args:
220
221
  engine: Database engine to use when executing queries.
221
- model: The ORM object to use for database manipulations.
222
+ table: The database table to query against.
222
223
 
223
224
  Returns:
224
- An async function that returns a single record from the given database model.
225
+ An async function that returns a single record from the given database table.
225
226
  """
226
227
 
227
- interface = create_db_interface(model)
228
+ interface = create_interface(table)
229
+ pk_interface = create_interface(table, pk_only=True, mode='required')
228
230
 
229
231
  async def get_record_handler(
230
- request: Request,
232
+ pk: pk_interface = Depends(),
231
233
  session: DBSession = Depends(create_session_iterator(engine)),
232
234
  ) -> interface:
233
235
  """Fetch a single record from the database."""
234
236
 
235
- query = select(model).filter_by(**request.path_params)
237
+ query = select(table).filter_by(**pk.model_dump())
236
238
  result = await execute_session_query(session, query)
237
239
  record = get_record_or_404(result)
238
- return interface.model_validate(record.__dict__)
240
+ return record
239
241
 
240
242
  return get_record_handler
241
243
 
242
244
 
243
- def create_post_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
245
+ def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
244
246
  """Create a function for handling POST requests against a record in the database.
245
247
 
246
248
  Args:
247
249
  engine: Database engine to use when executing queries.
248
- model: The ORM object to use for database manipulations.
250
+ table: The database table to query against.
249
251
 
250
252
  Returns:
251
253
  An async function that handles record creation.
252
254
  """
253
255
 
254
- interface = create_db_interface(model)
256
+ interface = create_interface(table)
255
257
 
256
258
  async def post_record_handler(
257
259
  data: interface,
258
260
  session: DBSession = Depends(create_session_iterator(engine)),
259
- ) -> interface:
261
+ ) -> None:
260
262
  """Create a new record in the database."""
261
263
 
262
- query = insert(model).values(**data.dict())
263
- result = await execute_session_query(session, query)
264
- record = get_record_or_404(result)
265
-
264
+ query = insert(table).values(**data.dict())
265
+ await execute_session_query(session, query)
266
266
  await commit_session(session)
267
- return interface.model_validate(record.__dict__)
268
267
 
269
268
  return post_record_handler
270
269
 
271
270
 
272
- def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
271
+ def create_put_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
273
272
  """Create a function for handling PUT requests against a record in the database.
274
273
 
275
274
  Args:
276
275
  engine: Database engine to use when executing queries.
277
- model: The ORM object to use for database manipulations.
276
+ table: The database table to query against.
278
277
 
279
278
  Returns:
280
279
  An async function that handles record updates.
281
280
  """
282
281
 
283
- interface = create_db_interface(model)
282
+ interface = create_interface(table)
283
+ opt_interface = create_interface(table, mode='optional')
284
+ pk_interface = create_interface(table, pk_only=True, mode='required')
284
285
 
285
286
  async def put_record_handler(
286
- request: Request,
287
- data: interface,
287
+ data: opt_interface,
288
+ pk: pk_interface = Depends(),
288
289
  session: DBSession = Depends(create_session_iterator(engine)),
289
290
  ) -> interface:
290
291
  """Replace record values in the database with the provided data."""
291
292
 
292
- query = select(model).filter_by(**request.path_params)
293
+ query = select(table).filter_by(**pk.model_dump())
293
294
  result = await execute_session_query(session, query)
294
295
  record = get_record_or_404(result)
295
296
 
296
- for key, value in data.dict().items():
297
+ for key, value in data.model_dump().items():
297
298
  setattr(record, key, value)
298
299
 
299
300
  await commit_session(session)
@@ -302,57 +303,60 @@ def create_put_record_handler(engine: DBEngine, model: DBModel) -> Callable[...,
302
303
  return put_record_handler
303
304
 
304
305
 
305
- def create_patch_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[PydanticModel]]:
306
+ def create_patch_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
306
307
  """Create a function for handling PATCH requests against a record in the database.
307
308
 
308
309
  Args:
309
310
  engine: Database engine to use when executing queries.
310
- model: The ORM object to use for database manipulations.
311
+ table: The database table to query against.
311
312
 
312
313
  Returns:
313
314
  An async function that handles record updates.
314
315
  """
315
316
 
316
- interface = create_db_interface(model)
317
+ interface = create_interface(table)
318
+ pk_interface = create_interface(table, pk_only=True, mode='required')
317
319
 
318
320
  async def patch_record_handler(
319
- request: Request,
320
321
  data: interface,
322
+ pk: pk_interface = Depends(),
321
323
  session: DBSession = Depends(create_session_iterator(engine)),
322
324
  ) -> interface:
323
325
  """Update record values in the database with the provided data."""
324
326
 
325
- query = select(model).filter_by(**request.path_params)
327
+ query = select(table).filter_by(**pk.model_dump())
326
328
  result = await execute_session_query(session, query)
327
329
  record = get_record_or_404(result)
328
330
 
329
- for key, value in data.dict(exclude_unset=True).items():
331
+ for key, value in data.model_dump(exclude_unset=True).items():
330
332
  setattr(record, key, value)
331
333
 
332
334
  await commit_session(session)
333
- return interface(record.__dict__)
335
+ return record
334
336
 
335
337
  return patch_record_handler
336
338
 
337
339
 
338
- def create_delete_record_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[None]]:
340
+ def create_delete_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[None]]:
339
341
  """Create a function for handling DELETE requests against a record in the database.
340
342
 
341
343
  Args:
342
344
  engine: Database engine to use when executing queries.
343
- model: The ORM object to use for database manipulations.
345
+ table: The database table to query against.
344
346
 
345
347
  Returns:
346
348
  An async function that handles record deletion.
347
349
  """
348
350
 
351
+ pk_interface = create_interface(table, pk_only=True, mode='required')
352
+
349
353
  async def delete_record_handler(
350
- request: Request,
354
+ pk: pk_interface = Depends(),
351
355
  session: DBSession = Depends(create_session_iterator(engine)),
352
356
  ) -> None:
353
357
  """Delete a record from the database."""
354
358
 
355
- query = select(model).filter_by(**request.path_params)
359
+ query = select(table).filter_by(**pk.model_dump())
356
360
  result = await execute_session_query(session, query)
357
361
  record = get_record_or_404(result)
358
362
 
@@ -0,0 +1,126 @@
1
+ """Pydantic models are used to facilitate data validation and to define
2
+ interfaces for FastAPI endpoint handlers. The `interfaces` module
3
+ provides utility functions for converting SQLAlchemy models into
4
+ Pydantic interfaces. Interfaces can be created using different modes
5
+ which force interface fields to be optional or read only.
6
+
7
+ !!! example "Example: Creating an Interface"
8
+
9
+ The `create_interface_default` method creates an interface class
10
+ based on a SQLAlchemy table.
11
+
12
+ ```python
13
+ default_interface = create_interface_default(database_model)
14
+ required_interface = create_interface_required(database_model, mode="required")
15
+ optional_interface = create_interface_optional(database_model, mode="optional")
16
+ ```
17
+ """
18
+ from typing import Iterator, Literal
19
+
20
+ from pydantic import BaseModel as PydanticModel, create_model
21
+ from sqlalchemy import Column, Table
22
+
23
+ __all__ = ["create_interface"]
24
+
25
+ from sqlalchemy.sql.schema import ScalarElementColumnDefault
26
+
27
+ MODES = Literal["default", "required", "optional"]
28
+
29
+
30
+ def iter_columns(table: Table, pk_only: bool = False) -> Iterator[Column]:
31
+ """Iterate over the columns of a SQLAlchemy model.
32
+
33
+ Args:
34
+ table: The table to iterate columns over.
35
+ pk_only: If True, only iterate over primary key columns.
36
+
37
+ Yields:
38
+ A column of the SQLAlchemy model.
39
+ """
40
+
41
+ for column in table.columns:
42
+ if column.primary_key or not pk_only:
43
+ yield column
44
+
45
+
46
+ def get_column_type(col: Column) -> type[any]:
47
+ """Return the Python type corresponding to a column's DB datatype.
48
+
49
+ Returns the `any` type for DBMS drivers that do not support mapping DB
50
+ types to Python primitives,
51
+
52
+ Args:
53
+ col: The column to determine a type for.
54
+
55
+ Returns:
56
+ The equivalent Python type for the column data.
57
+ """
58
+
59
+ try:
60
+ return col.type.python_type
61
+
62
+ # Catch any error, but list the expected ones explicitly
63
+ except (NotImplementedError, Exception):
64
+ return any
65
+
66
+
67
+ def get_column_default(col: Column, mode: MODES) -> any:
68
+ """Return the default value for a column.
69
+
70
+ Args:
71
+ col: The column to determine a default value for.
72
+ mode: The mode to use when determining the default value.
73
+
74
+ Returns:
75
+ The default value for the column.
76
+ """
77
+
78
+ # Extract the default value from the SQLAlchemy wrapper class
79
+ sqla_default = col.default
80
+ default = getattr(sqla_default, "arg", None) or sqla_default
81
+
82
+ if mode == "required":
83
+ return ...
84
+
85
+ elif mode == "optional":
86
+ return default
87
+
88
+ elif mode == "default":
89
+ if col.nullable or (col.default is not None):
90
+ return default
91
+
92
+ return ...
93
+
94
+ raise RuntimeError(f"Unknown mode: {mode}")
95
+
96
+
97
+ def create_interface(
98
+ table: Table,
99
+ pk_only: bool = False,
100
+ mode: MODES = "default"
101
+ ) -> type[PydanticModel]:
102
+ """Create a Pydantic interface for a SQLAlchemy model where all fields are required.
103
+
104
+ Args:
105
+ table: The SQLAlchemy table to create an interface for.
106
+ pk_only: If True, only include primary key columns.
107
+ mode: Whether to force fields to all be optional or required.
108
+
109
+ Returns:
110
+ A dynamically generated Pydantic model with all fields required.
111
+ """
112
+
113
+ # Map field names to the column type and default value.
114
+ columns = iter_columns(table, pk_only)
115
+ fields = {
116
+ col.name: (get_column_type(col), get_column_default(col, mode))
117
+ for col in columns
118
+ }
119
+
120
+ # Dynamically create a unique name for the interface
121
+ name_parts = [table.name, mode.title()]
122
+ if pk_only:
123
+ name_parts.insert(1, 'PK')
124
+
125
+ interface_name = '-'.join(name_parts)
126
+ return create_model(interface_name, **fields)
@@ -5,18 +5,16 @@ on the popular SQLAlchemy package, it natively supports multiple
5
5
  Database Management Systems (DBMS) without requiring custom configuration
6
6
  or setup.
7
7
 
8
- !!! example "Example: Creating ORM objects"
8
+ !!! example "Example: Mapping Database Metadata"
9
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.
10
+ Utility functions are provided for connecting to the database
11
+ and mapping the underlying schema.
13
12
 
14
13
  ```python
15
14
  connection_args = dict(...)
16
15
  db_url = create_db_url(**connection_args)
17
16
  db_conn = create_db_engine(db_url)
18
17
  db_meta = create_db_metadata(db_conn)
19
- db_models = create_db_models(db_meta)
20
18
  ```
21
19
 
22
20
  Support for asynchronous operations is automatically determined based on
@@ -36,29 +34,22 @@ import logging
36
34
  from pathlib import Path
37
35
  from typing import Callable
38
36
 
39
- from pydantic.main import create_model, BaseModel as PydanticModel
40
37
  from sqlalchemy import create_engine, Engine, MetaData, URL
41
38
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
42
- from sqlalchemy.orm import declarative_base, Session
39
+ from sqlalchemy.orm import Session
43
40
 
44
41
  __all__ = [
45
42
  "DBEngine",
46
- "DBModel",
47
43
  "DBSession",
48
44
  "create_db_engine",
49
- "create_db_interface",
50
45
  "create_db_metadata",
51
- "create_db_models",
52
46
  "create_db_url",
53
47
  "create_session_iterator",
54
48
  ]
55
49
 
56
50
  logger = logging.getLogger(__name__)
57
51
 
58
- Base = declarative_base()
59
-
60
52
  # Base classes and typing objects.
61
- DBModel = type[Base]
62
53
  DBEngine = Engine | AsyncEngine
63
54
  DBSession = Session | AsyncSession
64
55
 
@@ -133,7 +124,7 @@ async def _async_reflect_metadata(engine: AsyncEngine, metadata: MetaData) -> No
133
124
  """Helper function used to reflect database metadata using an async engine."""
134
125
 
135
126
  async with engine.connect() as connection:
136
- await connection.run_sync(metadata.reflect)
127
+ await connection.run_sync(metadata.reflect, views=True)
137
128
 
138
129
 
139
130
  def create_db_metadata(engine: DBEngine) -> MetaData:
@@ -153,55 +144,11 @@ def create_db_metadata(engine: DBEngine) -> MetaData:
153
144
  asyncio.run(_async_reflect_metadata(engine, metadata))
154
145
 
155
146
  else:
156
- metadata.reflect(bind=engine)
147
+ metadata.reflect(bind=engine, views=True)
157
148
 
158
149
  return metadata
159
150
 
160
151
 
161
- def create_db_models(metadata: MetaData) -> dict[str, DBModel]:
162
- """Dynamically generate database models from a metadata instance.
163
-
164
- Args:
165
- metadata: A reflection of database metadata.
166
-
167
- Returns:
168
- A dictionary mapping table names to database models.
169
- """
170
-
171
- logger.debug("Building database models...")
172
- models = {}
173
-
174
- # Dynamically create a class for each table.
175
- for table_name, table in metadata.tables.items():
176
- logger.debug(f"> Creating model for table {table_name}.")
177
- models[table_name] = type(
178
- table_name.capitalize(),
179
- (Base,),
180
- {"__table__": table},
181
- )
182
-
183
- logger.debug(f"Successfully generated {len(models)} models.")
184
- return models
185
-
186
-
187
- def create_db_interface(model: DBModel) -> type[PydanticModel]:
188
- """Create a Pydantic interface for a SQLAlchemy model.
189
-
190
- Args:
191
- model: A SQLAlchemy model to create an interface for.
192
-
193
- Returns:
194
- A Pydantic model class with the same structure as the provided SQLAlchemy model.
195
- """
196
-
197
- fields = {
198
- col.name: (col.type.python_type, col.default if col.default is not None else ...)
199
- for col in model.__table__.columns
200
- }
201
-
202
- return create_model(model.__name__, **fields)
203
-
204
-
205
152
  def create_session_iterator(engine: DBEngine) -> Callable[[], DBSession]:
206
153
  """Create a generator for database sessions.
207
154
 
@@ -29,16 +29,15 @@ validated arguments onto a SQLAlchemy query and returns the updated query.
29
29
  return ... # Logic to further process and execute the query goes here
30
30
  ```
31
31
  """
32
+
32
33
  from collections.abc import Callable
33
- from typing import Literal
34
+ from typing import Literal, Optional
34
35
 
35
36
  from fastapi import Depends, Query
36
- from sqlalchemy import asc, desc
37
+ from sqlalchemy import asc, desc, Table
37
38
  from sqlalchemy.sql.selectable import Select
38
39
  from starlette.responses import Response
39
40
 
40
- from .models import DBModel
41
-
42
41
  __all__ = [
43
42
  "apply_ordering_params",
44
43
  "apply_pagination_params",
@@ -47,21 +46,21 @@ __all__ = [
47
46
  ]
48
47
 
49
48
 
50
- def create_ordering_dependency(model: type[DBModel]) -> Callable[..., dict]:
49
+ def create_ordering_dependency(table: Table) -> Callable[..., dict]:
51
50
  """Create an injectable dependency for fetching ordering arguments from query parameters.
52
51
 
53
52
  Args:
54
- model: The database model to create the dependency for.
53
+ table: The database table to create the dependency for.
55
54
 
56
55
  Returns:
57
56
  An injectable FastAPI dependency.
58
57
  """
59
58
 
60
- columns = tuple(model.__table__.columns.keys())
59
+ columns = tuple(table.columns.keys())
61
60
 
62
61
  def get_ordering_params(
63
- _order_by_: Literal[*columns] = Query(None, description="The field name to sort by."),
64
- _direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
62
+ _order_by_: Optional[Literal[*columns]] = Query(None, description="The field name to sort by."),
63
+ _direction_: Optional[Literal["asc", "desc"]] = Query(None, description="Sort results in 'asc' or 'desc' order.")
65
64
  ) -> dict:
66
65
  """Extract ordering parameters from request query parameters.
67
66
 
@@ -114,19 +113,19 @@ def apply_ordering_params(query: Select, params: dict, response: Response) -> Se
114
113
  return query.order_by(asc(order_by))
115
114
 
116
115
 
117
- def create_pagination_dependency(model: type[DBModel]) -> Callable[..., dict]:
116
+ def create_pagination_dependency(table: Table) -> Callable[..., dict]:
118
117
  """Create an injectable dependency for fetching pagination arguments from query parameters.
119
118
 
120
119
  Args:
121
- model: The database model to create the dependency for.
120
+ table: The database table to create the dependency for.
122
121
 
123
122
  Returns:
124
123
  An injectable FastAPI dependency.
125
124
  """
126
125
 
127
126
  def get_pagination_params(
128
- _limit_: int = Query(0, ge=0, description="The maximum number of records to return."),
129
- _offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
127
+ _limit_: Optional[int] = Query(None, ge=0, description="The maximum number of records to return."),
128
+ _offset_: Optional[int] = Query(None, ge=0, description="The starting index of the returned records."),
130
129
  ) -> dict[str, int]:
131
130
  """Extract pagination parameters from request query parameters.
132
131
 
@@ -10,7 +10,7 @@ handling and provides a streamlined interface for database interactions.
10
10
  Query utilities seamlessly support synchronous and asynchronous session types.
11
11
 
12
12
  ```python
13
- query = select(SomeModel).where(SomeModel.id == item_id)
13
+ query = select(SomeTable).where(SomeTable.id == item_id)
14
14
 
15
15
  with Session(...) as sync_session:
16
16
  result = await execute_session_query(sync_session, query)
@@ -101,7 +101,7 @@ def get_record_or_404(result: Result) -> any:
101
101
  HTTPException: If the record is not found.
102
102
  """
103
103
 
104
- if not (record := result.scalar_one_or_none()):
105
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Record not found")
104
+ if record := result.fetchone():
105
+ return record
106
106
 
107
- return record
107
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Record not found")
@@ -0,0 +1,183 @@
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, Table
28
+ from starlette import status
29
+
30
+ from auto_rest.handlers import *
31
+ from auto_rest.models import DBEngine
32
+
33
+ __all__ = [
34
+ "create_meta_router",
35
+ "create_table_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(
49
+ path="/",
50
+ methods=["GET"],
51
+ endpoint=create_welcome_handler(),
52
+ include_in_schema=False
53
+ )
54
+
55
+ return router
56
+
57
+
58
+ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version: str) -> APIRouter:
59
+ """Create an API router for returning database metadata.
60
+
61
+ Includes routes for retrieving the database driver, database schema,
62
+ and application/schema version.
63
+
64
+ Args:
65
+ engine: The database engine used to facilitate database interactions.
66
+ metadata: The metadata object containing the database schema.
67
+ name: The application name.
68
+ version: The application versionnumber.
69
+
70
+ Returns:
71
+ An `APIRouter` with a routes for retrieving application metadata.
72
+ """
73
+
74
+ router = APIRouter()
75
+ tags = ["Application Metadata"]
76
+
77
+ router.add_api_route(
78
+ path="/app",
79
+ methods=["GET"],
80
+ endpoint=create_about_handler(name, version),
81
+ summary="Fetch application metadata.",
82
+ tags=tags
83
+ )
84
+
85
+ router.add_api_route(
86
+ path="/engine",
87
+ methods=["GET"],
88
+ endpoint=create_engine_handler(engine),
89
+ summary="Fetch database metadata.",
90
+ tags=tags
91
+ )
92
+
93
+ router.add_api_route(
94
+ path="/schema",
95
+ methods=["GET"],
96
+ endpoint=create_schema_handler(metadata),
97
+ summary="Fetch the database schema.",
98
+ tags=tags
99
+ )
100
+
101
+ return router
102
+
103
+
104
+ def create_table_router(engine: DBEngine, table: Table, writeable: bool = False) -> APIRouter:
105
+ """Create an API router with endpoint handlers for a given database table.
106
+
107
+ Args:
108
+ engine: The SQLAlchemy engine connected to the database.
109
+ table: The database table to create API endpoints for.
110
+ writeable: Whether the router should include support for write operations.
111
+
112
+ Returns:
113
+ An APIRouter instance with routes for database operations on the table.
114
+ """
115
+
116
+ router = APIRouter()
117
+
118
+ # Construct path parameters from primary key columns
119
+ pk_columns = sorted(column.name for column in table.primary_key.columns)
120
+ path_params_url = "/".join(f"{{{col_name}}}" for col_name in pk_columns)
121
+
122
+ # Add route for read operations against the table
123
+ router.add_api_route(
124
+ path="/",
125
+ methods=["GET"],
126
+ endpoint=create_list_records_handler(engine, table),
127
+ status_code=status.HTTP_200_OK,
128
+ summary="Fetch multiple records from the table.",
129
+ tags=[table.name],
130
+ )
131
+
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
+ )
142
+
143
+ # Add route for read operations against individual records
144
+ if pk_columns:
145
+ router.add_api_route(
146
+ path=f"/{path_params_url}/",
147
+ methods=["GET"],
148
+ endpoint=create_get_record_handler(engine, table),
149
+ status_code=status.HTTP_200_OK,
150
+ summary="Fetch a single record from the table.",
151
+ tags=[table.name],
152
+ )
153
+
154
+ # Add routes for write operations against individual records
155
+ if pk_columns and writeable:
156
+ router.add_api_route(
157
+ path=f"/{path_params_url}/",
158
+ methods=["PUT"],
159
+ endpoint=create_put_record_handler(engine, table),
160
+ status_code=status.HTTP_200_OK,
161
+ summary="Replace a single record in the table.",
162
+ tags=[table.name],
163
+ )
164
+
165
+ router.add_api_route(
166
+ path=f"/{path_params_url}/",
167
+ methods=["PATCH"],
168
+ endpoint=create_patch_record_handler(engine, table),
169
+ status_code=status.HTTP_200_OK,
170
+ summary="Update a single record in the table.",
171
+ tags=[table.name],
172
+ )
173
+
174
+ router.add_api_route(
175
+ path=f"/{path_params_url}/",
176
+ methods=["DELETE"],
177
+ endpoint=create_delete_record_handler(engine, table),
178
+ status_code=status.HTTP_200_OK,
179
+ summary="Delete a single record from the table.",
180
+ tags=[table.name],
181
+ )
182
+
183
+ return router
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "auto-rest-api"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  readme = "README.md"
9
9
  description = "Automatically map database schemas and deploy per-table REST API endpoints."
10
10
  authors = [{ name = "Better HPC LLC" }]
@@ -1,162 +0,0 @@
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
- name: 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 = sorted(column.name for column in model.__table__.primary_key.columns)
93
- path_params_url = "/".join(f"{{{col_name}}}" for col_name in pk_columns)
94
- path_params_openapi = {
95
- "parameters": [
96
- {"name": col_name, "in": "path", "required": True} for col_name in pk_columns
97
- ]
98
- }
99
-
100
- # Raise an error if no primary key columns are found
101
- # (SQLAlchemy should ensure this never happens)
102
- if not pk_columns: # pragma: no cover
103
- raise RuntimeError(f"No primary key columns found for table {model.__tablename__}.")
104
-
105
- # Define routes for read operations
106
- router.add_api_route(
107
- path="/",
108
- methods=["GET"],
109
- endpoint=create_list_records_handler(engine, model),
110
- status_code=status.HTTP_200_OK,
111
- tags=[model.__name__],
112
- )
113
-
114
- router.add_api_route(
115
- path=f"/{path_params_url}/",
116
- methods=["GET"],
117
- endpoint=create_get_record_handler(engine, model),
118
- status_code=status.HTTP_200_OK,
119
- tags=[model.__name__],
120
- openapi_extra=path_params_openapi
121
- )
122
-
123
- if not writeable:
124
- return router
125
-
126
- # Define routes for write operations
127
- router.add_api_route(
128
- path="/",
129
- methods=["POST"],
130
- endpoint=create_post_record_handler(engine, model),
131
- status_code=status.HTTP_201_CREATED,
132
- tags=[model.__name__],
133
- )
134
-
135
- router.add_api_route(
136
- path=f"/{path_params_url}/",
137
- methods=["PUT"],
138
- endpoint=create_put_record_handler(engine, model),
139
- status_code=status.HTTP_200_OK,
140
- tags=[model.__name__],
141
- openapi_extra=path_params_openapi
142
- )
143
-
144
- router.add_api_route(
145
- path=f"/{path_params_url}/",
146
- methods=["PATCH"],
147
- endpoint=create_patch_record_handler(engine, model),
148
- status_code=status.HTTP_200_OK,
149
- tags=[model.__name__],
150
- openapi_extra=path_params_openapi
151
- )
152
-
153
- router.add_api_route(
154
- path=f"/{path_params_url}/",
155
- methods=["DELETE"],
156
- endpoint=create_delete_record_handler(engine, model),
157
- status_code=status.HTTP_200_OK,
158
- tags=[model.__name__],
159
- openapi_extra=path_params_openapi
160
- )
161
-
162
- return router
File without changes
File without changes