auto-rest-api 0.1.0__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,11 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auto-rest-api
3
- Version: 0.1.0
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
7
7
  Author: Better HPC LLC
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11
9
9
  Classifier: Environment :: Web Environment
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Intended Audience :: Information Technology
@@ -3,10 +3,9 @@
3
3
  import logging
4
4
  from pathlib import Path
5
5
 
6
- import uvicorn
7
6
  import yaml
8
- from fastapi import FastAPI
9
7
 
8
+ from .app import *
10
9
  from .cli import *
11
10
  from .models import *
12
11
  from .routers import *
@@ -70,24 +69,26 @@ def run_application(
70
69
  app_version: version number for the generated OpenAPI schema.
71
70
  """
72
71
 
73
- # Connect to and map the database.
74
72
  logger.info(f"Mapping database schema for {db_name}.")
73
+
74
+ # Resolve database connection settings
75
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
76
  db_kwargs = yaml.safe_load(db_config.read_text()) if db_config else {}
77
+
78
+ # Connect to and map the database.
77
79
  db_conn = create_db_engine(db_url, **db_kwargs)
78
80
  db_meta = create_db_metadata(db_conn)
79
- db_models = create_db_models(db_meta)
80
81
 
81
82
  # Build an empty application and dynamically add the requested functionality.
82
83
  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 = create_app(app_title, app_version, enable_docs)
84
85
  app.include_router(create_welcome_router(), prefix="")
85
86
  app.include_router(create_meta_router(db_conn, db_meta, app_title, app_version), prefix="/meta")
86
87
 
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}")
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}")
90
91
 
91
92
  # Launch the API server.
92
93
  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")
94
+ run_server(app, server_host, server_port)
@@ -0,0 +1,67 @@
1
+ """
2
+ The `app` module provides factory functions and utilities for building and
3
+ deploying Fast-API applications.
4
+
5
+
6
+ !!! example "Example: Build and Deploy an API"
7
+
8
+ ```python
9
+ from auto_rest.app import create_app, run_server
10
+
11
+ app = create_app(app_title="My Application", app_version="1.2.3", enable_docs=True)
12
+ ... # Add endpoints to the application here
13
+ run_server(app, host="127.0.0.1", port=8081)
14
+ ```
15
+ """
16
+
17
+ import uvicorn
18
+ from fastapi import FastAPI
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+
21
+ __all__ = ["create_app", "run_server"]
22
+
23
+
24
+ def create_app(app_title: str, app_version: str, enable_docs: bool) -> FastAPI:
25
+ """Create and configure a FastAPI application instance.
26
+
27
+ This function initializes a FastAPI app with a customizable title, version,
28
+ and optional documentation routes. It also configures application middleware
29
+ for CORS policies.
30
+
31
+ Args:
32
+ app_title: The title of the FastAPI application.
33
+ app_version: The version of the FastAPI application.
34
+ enable_docs: Whether to enable the `/docs/` endpoint.
35
+
36
+ Returns:
37
+ FastAPI: A configured FastAPI application instance.
38
+ """
39
+
40
+ app = FastAPI(
41
+ title=app_title,
42
+ version=app_version,
43
+ docs_url="/docs/" if enable_docs else None,
44
+ redoc_url=None,
45
+ )
46
+
47
+ app.add_middleware(
48
+ CORSMiddleware,
49
+ allow_origins=["*"],
50
+ allow_credentials=True,
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+ return app
56
+
57
+
58
+ def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
59
+ """Deploy a FastAPI application server.
60
+
61
+ Args:
62
+ app: The FastAPI application to run.
63
+ host: The hostname or IP address for the server to bind to.
64
+ port: The port number for the server to listen on.
65
+ """
66
+
67
+ uvicorn.run(app, host=host, port=port, log_level="error")
@@ -55,10 +55,10 @@ from typing import Awaitable, Callable
55
55
 
56
56
  from fastapi import Depends, Response
57
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
58
+ from pydantic.main import BaseModel as PydanticModel
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 *
@@ -79,7 +79,7 @@ __all__ = [
79
79
  logger = logging.getLogger(__name__)
80
80
 
81
81
 
82
- def create_welcome_handler() -> Callable[[], Awaitable[ModelT]]:
82
+ def create_welcome_handler() -> Callable[[], Awaitable[PydanticModel]]:
83
83
  """Create an endpoint handler that returns an application welcome message.
84
84
 
85
85
  Returns:
@@ -96,7 +96,7 @@ def create_welcome_handler() -> Callable[[], Awaitable[ModelT]]:
96
96
  return welcome_handler
97
97
 
98
98
 
99
- def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[ModelT]]:
99
+ def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[PydanticModel]]:
100
100
  """Create an endpoint handler that returns the application name and version number.
101
101
 
102
102
  Args:
@@ -104,7 +104,7 @@ def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[Mode
104
104
  version: The returned version identifier.
105
105
 
106
106
  Returns:
107
- An async function that returns aplication info.
107
+ An async function that returns application info.
108
108
  """
109
109
 
110
110
  interface = create_model("Version", version=(str, version), name=(str, name))
@@ -117,7 +117,7 @@ def create_about_handler(name: str, version: str) -> Callable[[], Awaitable[Mode
117
117
  return about_handler
118
118
 
119
119
 
120
- def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[ModelT]]:
120
+ def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[PydanticModel]]:
121
121
  """Create an endpoint handler that returns configuration details for a database engine.
122
122
 
123
123
  Args:
@@ -141,7 +141,7 @@ def create_engine_handler(engine: DBEngine) -> Callable[[], Awaitable[ModelT]]:
141
141
  return meta_handler
142
142
 
143
143
 
144
- def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[ModelT]]:
144
+ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[PydanticModel]]:
145
145
  """Create an endpoint handler that returns the database schema.
146
146
 
147
147
  Args:
@@ -180,120 +180,121 @@ def create_schema_handler(metadata: MetaData) -> Callable[[], Awaitable[ModelT]]
180
180
  return schema_handler
181
181
 
182
182
 
183
- def create_list_records_handler(engine: DBEngine, model: DBModel) -> Callable[..., Awaitable[list[ModelT]]]:
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] = Depends(get_pagination_params),
200
- ordering_params: dict[str, int] = Depends(get_ordering_params),
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[ModelT]]:
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[ModelT]]:
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[ModelT]]:
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[ModelT]]:
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,19 +34,15 @@ import logging
36
34
  from pathlib import Path
37
35
  from typing import Callable
38
36
 
39
- from pydantic.main import create_model, ModelT
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
  ]
@@ -56,7 +50,6 @@ __all__ = [
56
50
  logger = logging.getLogger(__name__)
57
51
 
58
52
  # Base classes and typing objects.
59
- DBModel = declarative_base()
60
53
  DBEngine = Engine | AsyncEngine
61
54
  DBSession = Session | AsyncSession
62
55
 
@@ -131,7 +124,7 @@ async def _async_reflect_metadata(engine: AsyncEngine, metadata: MetaData) -> No
131
124
  """Helper function used to reflect database metadata using an async engine."""
132
125
 
133
126
  async with engine.connect() as connection:
134
- await connection.run_sync(metadata.reflect)
127
+ await connection.run_sync(metadata.reflect, views=True)
135
128
 
136
129
 
137
130
  def create_db_metadata(engine: DBEngine) -> MetaData:
@@ -151,55 +144,11 @@ def create_db_metadata(engine: DBEngine) -> MetaData:
151
144
  asyncio.run(_async_reflect_metadata(engine, metadata))
152
145
 
153
146
  else:
154
- metadata.reflect(bind=engine)
147
+ metadata.reflect(bind=engine, views=True)
155
148
 
156
149
  return metadata
157
150
 
158
151
 
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
152
  def create_session_iterator(engine: DBEngine) -> Callable[[], DBSession]:
204
153
  """Create a generator for database sessions.
205
154