auto-rest-api 0.1.2__tar.gz → 0.1.11__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.
@@ -617,59 +617,3 @@ Program, unless a warranty or assumption of liability accompanies a
617
617
  copy of the Program in return for a fee.
618
618
 
619
619
  END OF TERMS AND CONDITIONS
620
-
621
- ## How to Apply These Terms to Your New Programs
622
-
623
- If you develop a new program, and you want it to be of the greatest
624
- possible use to the public, the best way to achieve this is to make it
625
- free software which everyone can redistribute and change under these
626
- terms.
627
-
628
- To do so, attach the following notices to the program. It is safest to
629
- attach them to the start of each source file to most effectively state
630
- the exclusion of warranty; and each file should have at least the
631
- "copyright" line and a pointer to where the full notice is found.
632
-
633
- <one line to give the program's name and a brief idea of what it does.>
634
- Copyright (C) <year> <name of author>
635
-
636
- This program is free software: you can redistribute it and/or modify
637
- it under the terms of the GNU General Public License as published by
638
- the Free Software Foundation, either version 3 of the License, or
639
- (at your option) any later version.
640
-
641
- This program is distributed in the hope that it will be useful,
642
- but WITHOUT ANY WARRANTY; without even the implied warranty of
643
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
644
- GNU General Public License for more details.
645
-
646
- You should have received a copy of the GNU General Public License
647
- along with this program. If not, see <https://www.gnu.org/licenses/>.
648
-
649
- Also add information on how to contact you by electronic and paper
650
- mail.
651
-
652
- If the program does terminal interaction, make it output a short
653
- notice like this when it starts in an interactive mode:
654
-
655
- <program> Copyright (C) <year> <name of author>
656
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657
- This is free software, and you are welcome to redistribute it
658
- under certain conditions; type `show c' for details.
659
-
660
- The hypothetical commands \`show w' and \`show c' should show the
661
- appropriate parts of the General Public License. Of course, your
662
- program's commands might be different; for a GUI interface, you would
663
- use an "about box".
664
-
665
- You should also get your employer (if you work as a programmer) or
666
- school, if any, to sign a "copyright disclaimer" for the program, if
667
- necessary. For more information on this, and how to apply and follow
668
- the GNU GPL, see <https://www.gnu.org/licenses/>.
669
-
670
- The GNU General Public License does not permit incorporating your
671
- program into proprietary programs. If your program is a subroutine
672
- library, you may consider it more useful to permit linking proprietary
673
- applications with the library. If this is what you want to do, use the
674
- GNU Lesser General Public License instead of this License. But first,
675
- please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auto-rest-api
3
- Version: 0.1.2
3
+ Version: 0.1.11
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.11
8
+ Requires-Python: >=3.11,<4
9
9
  Classifier: Environment :: Web Environment
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Intended Audience :: Information Technology
@@ -18,23 +18,25 @@ Classifier: Topic :: Internet :: WWW/HTTP
18
18
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
19
19
  Classifier: Topic :: Software Development
20
20
  Classifier: Typing :: Typed
21
- Requires-Dist: aiomysql (>=0.2,<1.0)
22
- Requires-Dist: aioodbc (>=0.5,<1.0)
23
- Requires-Dist: aiosqlite (>=0.20,<1.0)
24
- Requires-Dist: asyncpg (>=0.30,<1.0)
25
- Requires-Dist: fastapi (>=0.115,<1.0)
26
- Requires-Dist: greenlet (>=3.1,<4.0)
27
- Requires-Dist: httpx (>=0.28,<1.0)
28
- Requires-Dist: oracledb (>=2.5,<3.0)
29
- Requires-Dist: pydantic (>=2.10,<3.0)
30
- Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
31
- Requires-Dist: sqlalchemy (>=2.0,<3.0)
32
- Requires-Dist: uvicorn (>=0.34,<1.0)
21
+ Requires-Dist: aiomysql (==0.3.2)
22
+ Requires-Dist: aioodbc (==0.5.0)
23
+ Requires-Dist: aiosqlite (==0.20.0)
24
+ Requires-Dist: asgi-correlation-id (==4.3.4)
25
+ Requires-Dist: asyncpg (==0.30.0)
26
+ Requires-Dist: colorlog (>=6.9.0,<7.0.0)
27
+ Requires-Dist: fastapi (==0.120.1)
28
+ Requires-Dist: greenlet (==3.2.4)
29
+ Requires-Dist: httpx (==0.28.1)
30
+ Requires-Dist: oracledb (==2.5.1)
31
+ Requires-Dist: pydantic (==2.12.3)
32
+ Requires-Dist: pyyaml (==6.0.3)
33
+ Requires-Dist: sqlalchemy (==2.0.44)
34
+ Requires-Dist: uvicorn (==0.38.0)
33
35
  Description-Content-Type: text/markdown
34
36
 
35
37
  # Auto-REST
36
38
 
37
- A command line tool for deploying dynamically generated REST APIs against relational databases.
39
+ A light-weight CLI tool for deploying dynamically generated REST APIs against relational databases.
38
40
  See the [project documentation](https://better-hpc.github.io/auto-rest/) for detailed usage instructions.
39
41
 
40
42
  ## Supported Databases
@@ -70,10 +72,6 @@ Launch an API by providing connection arguments to a database of your choice.
70
72
 
71
73
  ```shell
72
74
  auto-rest \
73
- # Enable optional endpoints / functionality
74
- --enable-docs \
75
- --enable-write \
76
- # Define the database type and connection arguments
77
75
  --psql
78
76
  --db-host localhost
79
77
  --db-port 5432
@@ -1,6 +1,6 @@
1
1
  # Auto-REST
2
2
 
3
- A command line tool for deploying dynamically generated REST APIs against relational databases.
3
+ A light-weight CLI tool for deploying dynamically generated REST APIs against relational databases.
4
4
  See the [project documentation](https://better-hpc.github.io/auto-rest/) for detailed usage instructions.
5
5
 
6
6
  ## Supported Databases
@@ -36,10 +36,6 @@ Launch an API by providing connection arguments to a database of your choice.
36
36
 
37
37
  ```shell
38
38
  auto-rest \
39
- # Enable optional endpoints / functionality
40
- --enable-docs \
41
- --enable-write \
42
- # Define the database type and connection arguments
43
39
  --psql
44
40
  --db-host localhost
45
41
  --db-port 5432
@@ -0,0 +1,70 @@
1
+ """Application entrypoint triggered by calling the packaged CLI command."""
2
+
3
+ import logging
4
+
5
+ from .app import *
6
+ from .cli import *
7
+ from .models import *
8
+ from .routers import *
9
+
10
+ __all__ = ["main", "run_application"]
11
+
12
+ logger = logging.getLogger("auto_rest")
13
+
14
+
15
+ def main() -> None: # pragma: no cover
16
+ """Application entry point called when executing the command line interface.
17
+
18
+ This is a wrapper around the `run_application` function used to provide
19
+ graceful error handling.
20
+ """
21
+
22
+ try:
23
+ run_application()
24
+
25
+ except KeyboardInterrupt:
26
+ pass
27
+
28
+ except Exception as e:
29
+ logger.critical(str(e), exc_info=True)
30
+
31
+
32
+ def run_application(cli_args: list[str] = None, /) -> None: # pragma: no cover
33
+ """Run an Auto-REST API server.
34
+
35
+ This function is equivalent to launching an API server from the command line
36
+ and accepts the same arguments as those provided in the CLI. Arguments are
37
+ parsed from STDIN by default, unless specified in the function call.
38
+
39
+ Args:
40
+ A list of commandline arguments used to run the application.
41
+ """
42
+
43
+ # Parse application arguments
44
+ args = create_cli_parser().parse_args(cli_args)
45
+ configure_cli_logging(args.log_level)
46
+
47
+ logger.info(f"Resolving database connection settings.")
48
+ db_kwargs = parse_db_settings(args.db_config)
49
+ db_url = create_db_url(
50
+ driver=args.db_driver,
51
+ host=args.db_host,
52
+ port=args.db_port,
53
+ database=args.db_name,
54
+ username=args.db_user,
55
+ password=args.db_pass
56
+ )
57
+
58
+ logger.info("Mapping database schema.")
59
+ db_conn = create_db_engine(db_url, **db_kwargs)
60
+ db_meta = create_db_metadata(db_conn)
61
+
62
+ logger.info("Creating application.")
63
+ app = create_app(args.app_title, args.app_version)
64
+ app.include_router(create_welcome_router(), prefix="")
65
+ app.include_router(create_meta_router(db_conn, db_meta, args.app_title, args.app_version), prefix="/meta")
66
+ for table_name, table in db_meta.tables.items():
67
+ app.include_router(create_table_router(db_conn, table), prefix=f"/db/{table_name}")
68
+
69
+ logger.info(f"Launching server on http://{args.server_host}:{args.server_port}.")
70
+ run_server(app, args.server_host, args.server_port)
@@ -0,0 +1,114 @@
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")
12
+ ... # Add endpoints to the application here
13
+ run_server(app, host="127.0.0.1", port=8081)
14
+ ```
15
+ """
16
+
17
+ import logging
18
+ from http import HTTPStatus
19
+
20
+ import uvicorn
21
+ from asgi_correlation_id import CorrelationIdMiddleware
22
+ from fastapi import FastAPI, Request
23
+ from fastapi.middleware.cors import CORSMiddleware
24
+ from starlette.responses import Response
25
+
26
+ __all__ = ["create_app", "run_server"]
27
+
28
+ logger = logging.getLogger("auto_rest.access")
29
+
30
+
31
+ async def logging_middleware(request: Request, call_next: callable) -> Response:
32
+ """FastAPI middleware for logging response status codes.
33
+
34
+ Args:
35
+ request: The incoming HTTP request.
36
+ call_next: The next middleware in the middleware chain.
37
+
38
+ Returns:
39
+ The outgoing HTTP response.
40
+ """
41
+
42
+ # Extract metadata from the request
43
+ request_meta = {
44
+ "ip": request.client.host,
45
+ "port": request.client.port,
46
+ "method": request.method,
47
+ "endpoint": request.url.path,
48
+ }
49
+
50
+ if request.url.query:
51
+ request_meta["endpoint"] += "?" + request.url.query
52
+
53
+ # Execute handling logic
54
+ try:
55
+ response = await call_next(request)
56
+
57
+ except Exception as exc:
58
+ logger.error(str(exc), exc_info=exc, extra=request_meta)
59
+ raise
60
+
61
+ # Log the outgoing response
62
+ status = HTTPStatus(response.status_code)
63
+ level = logging.INFO if status < 400 else logging.ERROR
64
+ logger.log(level, f"{status} {status.phrase}", extra=request_meta)
65
+
66
+ return response
67
+
68
+
69
+ def create_app(app_title: str, app_version: str) -> FastAPI:
70
+ """Create and configure a FastAPI application instance.
71
+
72
+ This function initializes a FastAPI app with a customizable title, version,
73
+ and optional documentation routes. It also configures application middleware
74
+ for CORS policies.
75
+
76
+ Args:
77
+ app_title: The title of the FastAPI application.
78
+ app_version: The version of the FastAPI application.
79
+
80
+ Returns:
81
+ FastAPI: A configured FastAPI application instance.
82
+ """
83
+
84
+ app = FastAPI(
85
+ title=app_title,
86
+ version=app_version,
87
+ docs_url="/docs/",
88
+ redoc_url=None,
89
+ )
90
+
91
+ app.middleware("http")(logging_middleware)
92
+ app.add_middleware(CorrelationIdMiddleware)
93
+ app.add_middleware(
94
+ CORSMiddleware,
95
+ allow_credentials=True,
96
+ allow_origins=["*"],
97
+ allow_methods=["*"],
98
+ allow_headers=["*"],
99
+ )
100
+
101
+ return app
102
+
103
+
104
+ def run_server(app: FastAPI, host: str, port: int) -> None: # pragma: no cover
105
+ """Deploy a FastAPI application server.
106
+
107
+ Args:
108
+ app: The FastAPI application to run.
109
+ host: The hostname or IP address for the server to bind to.
110
+ port: The port number for the server to listen on.
111
+ """
112
+
113
+ # Uvicorn overwrites its logging level when run and needs to be manually disabled here.
114
+ uvicorn.run(app, host=host, port=port, log_level=1000)
@@ -31,13 +31,11 @@ Python `logging` library.
31
31
  """
32
32
 
33
33
  import importlib.metadata
34
- import logging
34
+ import logging.config
35
35
  from argparse import ArgumentParser, HelpFormatter
36
36
  from pathlib import Path
37
37
 
38
- from uvicorn.logging import DefaultFormatter
39
-
40
- __all__ = ["VERSION", "configure_cli_logging", "create_cli_parser"]
38
+ __all__ = ["configure_cli_logging", "create_cli_parser", "VERSION"]
41
39
 
42
40
  VERSION = importlib.metadata.version("auto-rest-api")
43
41
 
@@ -49,7 +47,7 @@ def configure_cli_logging(level: str) -> None:
49
47
  logging configurations.
50
48
 
51
49
  Args:
52
- level: The Python logging level.
50
+ level: The Python logging level (e.g., "DEBUG", "INFO", etc.).
53
51
  """
54
52
 
55
53
  # Normalize and validate the logging level.
@@ -57,15 +55,57 @@ def configure_cli_logging(level: str) -> None:
57
55
  if level not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
58
56
  raise ValueError(f"Invalid logging level: {level}")
59
57
 
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
- )
58
+ msg_prefix = "%(log_color)s%(levelname)-8s%(reset)s (%(asctime)s) [%(correlation_id)s] "
59
+ logging.config.dictConfig({
60
+ "version": 1,
61
+ "disable_existing_loggers": True,
62
+ "filters": {
63
+ "correlation_id": {
64
+ "()": "asgi_correlation_id.CorrelationIdFilter",
65
+ "uuid_length": 8,
66
+ "default_value": "-" * 8
67
+ },
68
+ },
69
+ "formatters": {
70
+ "app": {
71
+ "()": "colorlog.ColoredFormatter",
72
+ "format": msg_prefix + "%(message)s",
73
+ },
74
+ "access": {
75
+ "()": "colorlog.ColoredFormatter",
76
+ "format": msg_prefix + "%(ip)s:%(port)s - %(method)s %(endpoint)s - %(message)s",
77
+ }
78
+ },
79
+ "handlers": {
80
+ "app": {
81
+ "class": "colorlog.StreamHandler",
82
+ "formatter": "app",
83
+ "filters": ["correlation_id"],
84
+ },
85
+ "access": {
86
+ "class": "colorlog.StreamHandler",
87
+ "formatter": "access",
88
+ "filters": ["correlation_id"],
89
+ }
90
+ },
91
+ "loggers": {
92
+ "auto_rest": {
93
+ "handlers": ["app"],
94
+ "level": level,
95
+ "propagate": False
96
+ },
97
+ "auto_rest.access": {
98
+ "handlers": ["access"],
99
+ "level": level,
100
+ "propagate": False
101
+ },
102
+ "auto_rest.query": {
103
+ "handlers": ["app"],
104
+ "level": level,
105
+ "propagate": False
106
+ }
107
+ }
108
+ })
69
109
 
70
110
 
71
111
  def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
@@ -95,10 +135,6 @@ def create_cli_parser(exit_on_error: bool = True) -> ArgumentParser:
95
135
  help="Set the logging level."
96
136
  )
97
137
 
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
138
  driver = parser.add_argument_group("database type")
103
139
  db_type = driver.add_mutually_exclusive_group(required=True)
104
140
  db_type.add_argument("--sqlite", action="store_const", dest="db_driver", const="sqlite+aiosqlite", help="use a SQLite database driver.")
@@ -50,17 +50,15 @@ This makes it easy to incorporate handlers into a FastAPI application.
50
50
  should always be tested within the context of a FastAPI application.
51
51
  """
52
52
 
53
- import logging
54
- from typing import Awaitable, Callable
53
+ from typing import Awaitable, Callable, Literal, Optional
55
54
 
56
- from fastapi import Depends, Response
55
+ from fastapi import Depends, Query, Response
57
56
  from pydantic import create_model
58
57
  from pydantic.main import BaseModel as PydanticModel
59
- from sqlalchemy import insert, MetaData, select, Table
58
+ from sqlalchemy import asc, desc, func, insert, MetaData, select, Table
60
59
 
61
60
  from .interfaces import *
62
61
  from .models import *
63
- from .params import *
64
62
  from .queries import *
65
63
 
66
64
  __all__ = [
@@ -76,8 +74,6 @@ __all__ = [
76
74
  "create_welcome_handler",
77
75
  ]
78
76
 
79
- logger = logging.getLogger(__name__)
80
-
81
77
 
82
78
  def create_welcome_handler() -> Callable[[], Awaitable[PydanticModel]]:
83
79
  """Create an endpoint handler that returns an application welcome message.
@@ -192,12 +188,17 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
192
188
  """
193
189
 
194
190
  interface = create_interface(table)
191
+ interface_opt = create_interface(table, mode="optional")
192
+ col_names = tuple(table.columns.keys())
195
193
 
196
194
  async def list_records_handler(
197
195
  response: Response,
198
196
  session: DBSession = Depends(create_session_iterator(engine)),
199
- pagination_params: dict[str, int] = create_pagination_dependency(table),
200
- ordering_params: dict[str, int] = create_ordering_dependency(table),
197
+ filters: interface_opt = Depends(),
198
+ _limit_: int = Query(0, ge=0, description="The maximum number of records to return."),
199
+ _offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
200
+ _order_by_: Optional[Literal[*col_names]] = Query(None, description="The field name to sort by."),
201
+ _direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order."),
201
202
  ) -> list[interface]:
202
203
  """Fetch a list of records from the database.
203
204
 
@@ -205,11 +206,39 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
205
206
  """
206
207
 
207
208
  query = select(table)
208
- query = apply_pagination_params(query, pagination_params, response)
209
- query = apply_ordering_params(query, ordering_params, response)
210
209
 
211
- result = await execute_session_query(session, query)
212
- return [row._mapping for row in result.all()]
210
+ # Fetch data per the request parameters
211
+ for param, value in filters:
212
+ if value is not None:
213
+ column = getattr(table.c, param)
214
+
215
+ # Use a "_null_" parameter value to represent a `None` database value
216
+ if isinstance(value, str) and value.lower() == "_null_":
217
+ query = query.filter(column.is_(None))
218
+
219
+ else:
220
+ query = query.filter(column.ilike(f"%{value}%"))
221
+
222
+ if _limit_ > 0:
223
+ query = query.offset(_offset_).limit(_limit_)
224
+
225
+ if _order_by_ is not None:
226
+ direction = {'desc': desc, 'asc': asc}[_direction_]
227
+ query = query.order_by(direction(_order_by_))
228
+
229
+ # Determine total record count
230
+ total_count_query = select(func.count()).select_from(table)
231
+ total_count = await execute_session_query(session, total_count_query)
232
+
233
+ # Set headers
234
+ response.headers["x-pagination-limit"] = str(_limit_)
235
+ response.headers["x-pagination-offset"] = str(_offset_)
236
+ response.headers["x-pagination-total"] = str(total_count.first()[0])
237
+ response.headers["x-order-by"] = str(_order_by_)
238
+ response.headers["x-order-direction"] = str(_direction_)
239
+
240
+ # noinspection PyTypeChecker
241
+ return await execute_session_query(session, query)
213
242
 
214
243
  return list_records_handler
215
244
 
@@ -242,7 +271,7 @@ def create_get_record_handler(engine: DBEngine, table: Table) -> Callable[..., A
242
271
  return get_record_handler
243
272
 
244
273
 
245
- def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[PydanticModel]]:
274
+ def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[..., Awaitable[None]]:
246
275
  """Create a function for handling POST requests against a record in the database.
247
276
 
248
277
  Args:
@@ -261,7 +290,7 @@ def create_post_record_handler(engine: DBEngine, table: Table) -> Callable[...,
261
290
  ) -> None:
262
291
  """Create a new record in the database."""
263
292
 
264
- query = insert(table).values(**data.dict())
293
+ query = insert(table).values(**data.model_dump())
265
294
  await execute_session_query(session, query)
266
295
  await commit_session(session)
267
296
 
@@ -298,7 +327,7 @@ def create_put_record_handler(engine: DBEngine, table: Table) -> Callable[..., A
298
327
  setattr(record, key, value)
299
328
 
300
329
  await commit_session(session)
301
- return interface.model_validate(record.__dict__)
330
+ return record
302
331
 
303
332
  return put_record_handler
304
333
 
@@ -0,0 +1,118 @@
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` method creates a Pydantic interface class derived
10
+ from a SQLAlchemy table. By default, interface fields are marked as
11
+ required based on the underlying schema. The `mode` argument is used to
12
+ override field definitions and make all fields in the interface either
13
+ `optional` or `required` regardless of the schema definition.
14
+
15
+ ```python
16
+ default_interface = create_interface(database_model)
17
+ required_interface = create_interface(database_model, mode="required")
18
+ optional_interface = create_interface(database_model, mode="optional")
19
+ ```
20
+ """
21
+
22
+ from typing import Any, Iterator, Literal
23
+
24
+ from pydantic import BaseModel as PydanticModel, create_model
25
+ from sqlalchemy import Column, Table
26
+
27
+ __all__ = ["create_interface"]
28
+
29
+ MODE_TYPE = Literal["default", "required", "optional"]
30
+
31
+
32
+ def iter_columns(table: Table, pk_only: bool = False) -> Iterator[Column]:
33
+ """Iterate over the columns of a SQLAlchemy model.
34
+
35
+ Args:
36
+ table: The table to iterate columns over.
37
+ pk_only: If True, only iterate over primary key columns.
38
+
39
+ Yields:
40
+ A column of the SQLAlchemy model.
41
+ """
42
+
43
+ for column in table.columns.values():
44
+ if column.primary_key or not pk_only:
45
+ yield column
46
+
47
+
48
+ def create_field_definition(col: Column, mode: MODE_TYPE = "default") -> tuple[type[any], any]:
49
+ """Return a tuple with the type and default value for a database table column.
50
+
51
+ The returned tuple is compatible for use with Pydantic as a field definition
52
+ during dynamic model generation. The `mode` argument modifies returned
53
+ values to enforce different behavior in the generated Pydantic interface.
54
+
55
+ Modes:
56
+ default: Values are marked as (not)required based on the column schema.
57
+ required: Values are always marked required.
58
+ optional: Values are always marked optional.
59
+
60
+ Args:
61
+ col: The column to return values for.
62
+ mode: The mode to use when determining the default value.
63
+
64
+ Returns:
65
+ The default value for the column.
66
+ """
67
+
68
+ try:
69
+ col_type = col.type.python_type
70
+
71
+ except NotImplementedError:
72
+ col_type = Any
73
+
74
+ col_default = getattr(col.default, "arg", col.default)
75
+
76
+ if mode == "required":
77
+ return col_type, ...
78
+
79
+ elif mode == "optional":
80
+ return col_type | None, col_default
81
+
82
+ elif mode == "default" and (col.nullable or col.default):
83
+ return col_type | None, col_default
84
+
85
+ elif mode == "default":
86
+ return col_type, ...
87
+
88
+ raise RuntimeError(f"Unknown mode: {mode}")
89
+
90
+
91
+ def create_interface(table: Table, pk_only: bool = False, mode: MODE_TYPE = "default") -> type[PydanticModel]:
92
+ """Create a Pydantic interface for a SQLAlchemy model where all fields are required.
93
+
94
+ Modes:
95
+ default: Values are marked as (not)required based on the column schema.
96
+ required: Values are always marked required.
97
+ optional: Values are always marked optional.
98
+
99
+ Args:
100
+ table: The SQLAlchemy table to create an interface for.
101
+ pk_only: If True, only include primary key columns.
102
+ mode: Whether to force fields to all be optional or required.
103
+
104
+ Returns:
105
+ A dynamically generated Pydantic model with all fields required.
106
+ """
107
+
108
+ # Map field names to the column type and default value.
109
+ fields = {
110
+ col.name: create_field_definition(col, mode) for col in iter_columns(table, pk_only)
111
+ }
112
+
113
+ # Create a unique name for the interface
114
+ name = f"{table.name}-{mode.title()}"
115
+ if pk_only:
116
+ name += '-PK'
117
+
118
+ return create_model(name, __config__={'arbitrary_types_allowed': True}, **fields)