auto-rest-api 0.1.2__py3-none-any.whl → 0.1.3__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/handlers.py CHANGED
@@ -51,16 +51,15 @@ This makes it easy to incorporate handlers into a FastAPI application.
51
51
  """
52
52
 
53
53
  import logging
54
- from typing import Awaitable, Callable
54
+ from typing import Awaitable, Callable, Literal, Optional
55
55
 
56
- from fastapi import Depends, Response
56
+ from fastapi import Depends, Query, Response
57
57
  from pydantic import create_model
58
58
  from pydantic.main import BaseModel as PydanticModel
59
59
  from sqlalchemy import insert, MetaData, select, Table
60
60
 
61
61
  from .interfaces import *
62
62
  from .models import *
63
- from .params import *
64
63
  from .queries import *
65
64
 
66
65
  __all__ = [
@@ -192,21 +191,30 @@ def create_list_records_handler(engine: DBEngine, table: Table) -> Callable[...,
192
191
  """
193
192
 
194
193
  interface = create_interface(table)
194
+ interface_opt = create_interface(table, mode="optional")
195
+ columns = tuple(table.columns.keys())
195
196
 
196
197
  async def list_records_handler(
197
198
  response: Response,
198
199
  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),
200
+ _limit_: int = Query(0, ge=0, description="The maximum number of records to return."),
201
+ _offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
202
+ _order_by_: Optional[Literal[*columns]] = Query(None, description="The field name to sort by."),
203
+ _direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
201
204
  ) -> list[interface]:
202
205
  """Fetch a list of records from the database.
203
206
 
204
207
  URL query parameters are used to enable filtering, ordering, and paginating returned values.
205
208
  """
206
209
 
210
+ response.headers["X-Pagination-Limit"] = str(_limit_)
211
+ response.headers["X-Pagination-Offset"] = str(_offset_)
212
+ response.headers["X-Order-By"] = str(_order_by_)
213
+ response.headers["X-Order-Direction"] = str(_direction_)
214
+
207
215
  query = select(table)
208
- query = apply_pagination_params(query, pagination_params, response)
209
- query = apply_ordering_params(query, ordering_params, response)
216
+ query = apply_pagination_params(query, _limit_, _offset_)
217
+ query = apply_ordering_params(query, _order_by_, _direction_)
210
218
 
211
219
  result = await execute_session_query(session, query)
212
220
  return [row._mapping for row in result.all()]
auto_rest/interfaces.py CHANGED
@@ -10,21 +10,20 @@ which force interface fields to be optional or read only.
10
10
  based on a SQLAlchemy table.
11
11
 
12
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")
13
+ default_interface = create_interface(database_model)
14
+ required_interface = create_interface(database_model, mode="required")
15
+ optional_interface = create_interface(database_model, mode="optional")
16
16
  ```
17
17
  """
18
- from typing import Iterator, Literal
18
+
19
+ from typing import Any, Iterator, Literal
19
20
 
20
21
  from pydantic import BaseModel as PydanticModel, create_model
21
22
  from sqlalchemy import Column, Table
22
23
 
23
24
  __all__ = ["create_interface"]
24
25
 
25
- from sqlalchemy.sql.schema import ScalarElementColumnDefault
26
-
27
- MODES = Literal["default", "required", "optional"]
26
+ MODE_TYPE = Literal["default", "required", "optional"]
28
27
 
29
28
 
30
29
  def iter_columns(table: Table, pk_only: bool = False) -> Iterator[Column]:
@@ -38,69 +37,62 @@ def iter_columns(table: Table, pk_only: bool = False) -> Iterator[Column]:
38
37
  A column of the SQLAlchemy model.
39
38
  """
40
39
 
41
- for column in table.columns:
40
+ for column in table.columns.values():
42
41
  if column.primary_key or not pk_only:
43
42
  yield column
44
43
 
45
44
 
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.
45
+ def create_field_definition(col: Column, mode: MODE_TYPE = "default") -> tuple[type[any], any]:
46
+ """Return a tuple with the type and default value for a database table column.
54
47
 
55
- Returns:
56
- The equivalent Python type for the column data.
57
- """
58
-
59
- try:
60
- return col.type.python_type
48
+ The returned tuple is compatible for use with Pydantic as a field definition
49
+ during dynamic model generation. The `mode` argument modifies returned
50
+ values to enforce different behavior in the generated Pydantic interface.
61
51
 
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.
52
+ Modes:
53
+ default: Values are marked as (not)required based on the column schema.
54
+ required: Values are always marked required.
55
+ required: Values are always marked optional.
69
56
 
70
57
  Args:
71
- col: The column to determine a default value for.
58
+ col: The column to return values for.
72
59
  mode: The mode to use when determining the default value.
73
60
 
74
61
  Returns:
75
62
  The default value for the column.
76
63
  """
77
64
 
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
65
+ try:
66
+ col_type = col.type.python_type
67
+
68
+ except NotImplementedError:
69
+ col_type = Any
70
+
71
+ col_default = getattr(col.default, "arg", col.default)
81
72
 
82
73
  if mode == "required":
83
- return ...
74
+ return col_type, ...
84
75
 
85
76
  elif mode == "optional":
86
- return default
77
+ return col_type | None, col_default
87
78
 
88
- elif mode == "default":
89
- if col.nullable or (col.default is not None):
90
- return default
79
+ elif mode == "default" and (col.nullable or col.default):
80
+ return col_type | None, col_default
91
81
 
92
- return ...
82
+ elif mode == "default":
83
+ return col_type, ...
93
84
 
94
85
  raise RuntimeError(f"Unknown mode: {mode}")
95
86
 
96
87
 
97
- def create_interface(
98
- table: Table,
99
- pk_only: bool = False,
100
- mode: MODES = "default"
101
- ) -> type[PydanticModel]:
88
+ def create_interface(table: Table, pk_only: bool = False, mode: MODE_TYPE = "default") -> type[PydanticModel]:
102
89
  """Create a Pydantic interface for a SQLAlchemy model where all fields are required.
103
90
 
91
+ Modes:
92
+ default: Values are marked as (not)required based on the column schema.
93
+ required: Values are always marked required.
94
+ required: Values are always marked optional.
95
+
104
96
  Args:
105
97
  table: The SQLAlchemy table to create an interface for.
106
98
  pk_only: If True, only include primary key columns.
@@ -111,16 +103,13 @@ def create_interface(
111
103
  """
112
104
 
113
105
  # Map field names to the column type and default value.
114
- columns = iter_columns(table, pk_only)
115
106
  fields = {
116
- col.name: (get_column_type(col), get_column_default(col, mode))
117
- for col in columns
107
+ col.name: create_field_definition(col, mode) for col in iter_columns(table, pk_only)
118
108
  }
119
109
 
120
- # Dynamically create a unique name for the interface
121
- name_parts = [table.name, mode.title()]
110
+ # Create a unique name for the interface
111
+ name = f"{table.name}-{mode.title()}"
122
112
  if pk_only:
123
- name_parts.insert(1, 'PK')
113
+ name += '-PK'
124
114
 
125
- interface_name = '-'.join(name_parts)
126
- return create_model(interface_name, **fields)
115
+ return create_model(name, __config__={'arbitrary_types_allowed': True}, **fields)
auto_rest/queries.py CHANGED
@@ -19,15 +19,18 @@ handling and provides a streamlined interface for database interactions.
19
19
  result = await execute_session_query(async_session, query)
20
20
  ```
21
21
  """
22
+ from typing import Literal
22
23
 
23
24
  from fastapi import HTTPException
24
- from sqlalchemy import Executable, Result
25
+ from sqlalchemy import asc, desc, Executable, Result, Select
25
26
  from sqlalchemy.ext.asyncio import AsyncSession
26
27
  from starlette import status
27
28
 
28
29
  from auto_rest.models import DBSession
29
30
 
30
31
  __all__ = [
32
+ "apply_ordering_params",
33
+ "apply_pagination_params",
31
34
  "commit_session",
32
35
  "delete_session_record",
33
36
  "execute_session_query",
@@ -35,6 +38,64 @@ __all__ = [
35
38
  ]
36
39
 
37
40
 
41
+ def apply_ordering_params(
42
+ query: Select,
43
+ order_by: str | None = None,
44
+ direction: Literal["desc", "asc"] = "asc"
45
+ ) -> Select:
46
+ """Apply ordering to a database query.
47
+
48
+ Returns a copy of the provided query with ordering parameters applied.
49
+
50
+ Args:
51
+ query: The database query to apply parameters to.
52
+ order_by: The name of the column to order by.
53
+ direction: The direction to order by (defaults to "asc").
54
+
55
+ Returns:
56
+ A copy of the query modified to return ordered values.
57
+ """
58
+
59
+ if order_by is None:
60
+ return query
61
+
62
+ if order_by not in query.columns:
63
+ raise ValueError(f"Invalid column name: {order_by}")
64
+
65
+ # Default to ascending order for an invalid ordering direction
66
+ if direction == "desc":
67
+ return query.order_by(desc(order_by))
68
+
69
+ elif direction == "asc":
70
+ return query.order_by(asc(order_by))
71
+
72
+ raise ValueError(f"Invalid direction, use 'asc' or 'desc': {direction}")
73
+
74
+
75
+ def apply_pagination_params(query: Select, limit: int = 0, offset: int = 0) -> Select:
76
+ """Apply pagination to a database query.
77
+
78
+ Returns a copy of the provided query with offset and limit parameters applied.
79
+
80
+ Args:
81
+ query: The database query to apply parameters to.
82
+ limit: The number of results to return.
83
+ offset: The offset to start with.
84
+
85
+ Returns:
86
+ A copy of the query modified to only return the paginated values.
87
+ """
88
+
89
+ if offset < 0 or limit < 0:
90
+ raise ValueError("Pagination parameters cannot be negative")
91
+
92
+ # Do not apply pagination if not requested
93
+ if limit == 0:
94
+ return query
95
+
96
+ return query.offset(offset or 0).limit(limit)
97
+
98
+
38
99
  async def commit_session(session: DBSession) -> None:
39
100
  """Commit a SQLAlchemy session.
40
101
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auto-rest-api
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -0,0 +1,14 @@
1
+ auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
2
+ auto_rest/__main__.py,sha256=lz6-LEpbu51Ah3QCzUzZhevjWhK5kmOxhuuqhX5t-io,3093
3
+ auto_rest/app.py,sha256=I6ZeHzKuhPTS1svLhMTvefjzTkMJgGpwVz1kdJ_9mtE,1887
4
+ auto_rest/cli.py,sha256=A7-kMTNNzqZB7jTUJBAVqfYm9RyYjENr0g_vK9JE0N4,5199
5
+ auto_rest/handlers.py,sha256=Z8DMv3KRYLBXlUKoE4e-2qE9YvG-IPgS57C0RpUDWv4,12603
6
+ auto_rest/interfaces.py,sha256=WB_0eMDjGF8DpnDN9INHqo7u4x3aklvzAYK4t3JwC7s,3779
7
+ auto_rest/models.py,sha256=HCJUQPBmkkfVFv9CRcPzMP66pQk_UIb3j3cdwHqodwE,5388
8
+ auto_rest/queries.py,sha256=Z2ATkcSYldV6BkcrmLogmBoDkPEkyFzFqI7qPcq86uc,4705
9
+ auto_rest/routers.py,sha256=RqBwLqVFU1OIFSCUuwOtTis8mT_zyWTaB_8SyIXjfe0,5727
10
+ auto_rest_api-0.1.3.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
11
+ auto_rest_api-0.1.3.dist-info/METADATA,sha256=Cs7u-2jZGTTlvBprPzdLVWl1uSAAjjLgeh5PuHo3tUk,2951
12
+ auto_rest_api-0.1.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
13
+ auto_rest_api-0.1.3.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
14
+ auto_rest_api-0.1.3.dist-info/RECORD,,
auto_rest/params.py DELETED
@@ -1,174 +0,0 @@
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 of two. The first function is a
8
- factory for creating an injectable FastAPI dependency. The dependency
9
- is used to parse parameters from incoming requests and applies high level
10
- validation against the parsed values. The second function to applies the
11
- validated arguments onto a SQLAlchemy query and returns the updated query.
12
-
13
- !!! example "Example: Parameter Parsing and Application"
14
-
15
- ```python
16
- from fastapi import FastAPI, Response
17
- from sqlalchemy import select
18
- from auto_rest.query_params import create_pagination_dependency, apply_pagination_params
19
-
20
- app = FastAPI()
21
-
22
- @app.get("/items/")
23
- async def list_items(
24
- pagination_params: dict = create_pagination_dependency(model),
25
- response: Response
26
- ):
27
- query = select(model)
28
- query = apply_pagination_params(query, pagination_params, response)
29
- return ... # Logic to further process and execute the query goes here
30
- ```
31
- """
32
-
33
- from collections.abc import Callable
34
- from typing import Literal, Optional
35
-
36
- from fastapi import Depends, Query
37
- from sqlalchemy import asc, desc, Table
38
- from sqlalchemy.sql.selectable import Select
39
- from starlette.responses import Response
40
-
41
- __all__ = [
42
- "apply_ordering_params",
43
- "apply_pagination_params",
44
- "create_ordering_dependency",
45
- "create_pagination_dependency",
46
- ]
47
-
48
-
49
- def create_ordering_dependency(table: Table) -> Callable[..., dict]:
50
- """Create an injectable dependency for fetching ordering arguments from query parameters.
51
-
52
- Args:
53
- table: The database table to create the dependency for.
54
-
55
- Returns:
56
- An injectable FastAPI dependency.
57
- """
58
-
59
- columns = tuple(table.columns.keys())
60
-
61
- def get_ordering_params(
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.")
64
- ) -> dict:
65
- """Extract ordering parameters from request query parameters.
66
-
67
- Args:
68
- _order_by_: The field to order by.
69
- _direction_: The direction to order by.
70
-
71
- Returns:
72
- dict: A dictionary containing the `order_by` and `direction` values.
73
- """
74
-
75
- return {"order_by": _order_by_, "direction": _direction_}
76
-
77
- return Depends(get_ordering_params)
78
-
79
-
80
- def apply_ordering_params(query: Select, params: dict, response: Response) -> Select:
81
- """Apply ordering to a database query.
82
-
83
- Returns a copy of the provided query with ordering parameters applied.
84
- This method is compatible with parameters returned by the `get_ordering_params` method.
85
- Ordering is not applied for invalid params, but response headers are still set.
86
-
87
- Args:
88
- query: The database query to apply parameters to.
89
- params: A dictionary containing parsed URL parameters.
90
- response: The outgoing HTTP response object.
91
-
92
- Returns:
93
- A copy of the query modified to return ordered values.
94
- """
95
-
96
- order_by = params.get("order_by")
97
- direction = params.get("direction")
98
-
99
- # Set common response headers
100
- response.headers["X-Order-By"] = str(order_by)
101
- response.headers["X-Order-Direction"] = str(direction)
102
-
103
- if order_by is None:
104
- response.headers["X-Order-Applied"] = "false"
105
- return query
106
-
107
- # Default to ascending order for an invalid ordering direction
108
- response.headers["X-Order-Applied"] = "true"
109
- if direction == "desc":
110
- return query.order_by(desc(order_by))
111
-
112
- else:
113
- return query.order_by(asc(order_by))
114
-
115
-
116
- def create_pagination_dependency(table: Table) -> Callable[..., dict]:
117
- """Create an injectable dependency for fetching pagination arguments from query parameters.
118
-
119
- Args:
120
- table: The database table to create the dependency for.
121
-
122
- Returns:
123
- An injectable FastAPI dependency.
124
- """
125
-
126
- def get_pagination_params(
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."),
129
- ) -> dict[str, int]:
130
- """Extract pagination parameters from request query parameters.
131
-
132
- Args:
133
- _limit_: The maximum number of records to return.
134
- _offset_: The starting index of the returned records.
135
-
136
- Returns:
137
- dict: A dictionary containing the `limit` and `offset` values.
138
- """
139
-
140
- return {"limit": _limit_, "offset": _offset_}
141
-
142
- return Depends(get_pagination_params)
143
-
144
-
145
- def apply_pagination_params(query: Select, params: dict[str, int], response: Response) -> Select:
146
- """Apply pagination to a database query.
147
-
148
- Returns a copy of the provided query with offset and limit parameters applied.
149
- This method is compatible with parameters returned by the `get_pagination_params` method.
150
- Pagination is not applied for invalid params, but response headers are still set.
151
-
152
- Args:
153
- query: The database query to apply parameters to.
154
- params: A dictionary containing parsed URL parameters.
155
- response: The outgoing HTTP response object.
156
-
157
- Returns:
158
- A copy of the query modified to only return the paginated values.
159
- """
160
-
161
- limit = params.get("limit")
162
- offset = params.get("offset")
163
-
164
- # Set common response headers
165
- response.headers["X-Pagination-Limit"] = str(limit)
166
- response.headers["X-Pagination-Offset"] = str(offset)
167
-
168
- # Do not apply pagination if not requested
169
- if limit in (0, None):
170
- response.headers["X-Pagination-Applied"] = "false"
171
- return query
172
-
173
- response.headers["X-Pagination-Applied"] = "true"
174
- return query.offset(offset or 0).limit(limit)
@@ -1,15 +0,0 @@
1
- auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
2
- auto_rest/__main__.py,sha256=lz6-LEpbu51Ah3QCzUzZhevjWhK5kmOxhuuqhX5t-io,3093
3
- auto_rest/app.py,sha256=I6ZeHzKuhPTS1svLhMTvefjzTkMJgGpwVz1kdJ_9mtE,1887
4
- auto_rest/cli.py,sha256=A7-kMTNNzqZB7jTUJBAVqfYm9RyYjENr0g_vK9JE0N4,5199
5
- auto_rest/handlers.py,sha256=84P7mEXzwpEgSOg6ZIBMT_q0ZTfiESpd-IXUmekMqJo,12005
6
- auto_rest/interfaces.py,sha256=U2-e3fy_ROmNrIAvkXG7dACMJi3akFEldXrbIacMbIc,3714
7
- auto_rest/models.py,sha256=HCJUQPBmkkfVFv9CRcPzMP66pQk_UIb3j3cdwHqodwE,5388
8
- auto_rest/params.py,sha256=t1YK9Q-dwh7N99myYH2RZCO6XheWcgLJl_Stpi65PKQ,6075
9
- auto_rest/queries.py,sha256=nws0J1XnCzB3Y0DN1dDccsgK91z7dzza1VyC_qCitG0,2910
10
- auto_rest/routers.py,sha256=RqBwLqVFU1OIFSCUuwOtTis8mT_zyWTaB_8SyIXjfe0,5727
11
- auto_rest_api-0.1.2.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
12
- auto_rest_api-0.1.2.dist-info/METADATA,sha256=xQY6_P1nDZbq6PfcDp5EGjEhRxCwJql_g8V0NcRpxVM,2951
13
- auto_rest_api-0.1.2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
14
- auto_rest_api-0.1.2.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
15
- auto_rest_api-0.1.2.dist-info/RECORD,,