auto-rest-api 0.1.0__py3-none-any.whl → 0.1.2__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/__main__.py CHANGED
@@ -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)
auto_rest/app.py ADDED
@@ -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")
auto_rest/handlers.py CHANGED
@@ -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)
auto_rest/models.py CHANGED
@@ -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
 
auto_rest/params.py CHANGED
@@ -4,71 +4,85 @@ parameters from incoming HTTP requests. These utilities ensure the consistent
4
4
  parsing, validation, and application of query parameters, and automatically
5
5
  update HTTP response headers to reflect applied query options.
6
6
 
7
- Parameter functions are designed in pairs. A *get* function is used to parse
8
- parameters from a FastAPI request and an *apply* function is used to apply
9
- the arguments onto a SQLAlchemy query.
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.
10
12
 
11
13
  !!! example "Example: Parameter Parsing and Application"
12
14
 
13
- When handling URL parameters, a *get* function is injected as a dependency
14
- into the signature of the request handler. The parsed parameter dictionary
15
- is then passed to an *apply* function.
16
-
17
15
  ```python
18
- from fastapi import FastAPI, Response, Depends
16
+ from fastapi import FastAPI, Response
19
17
  from sqlalchemy import select
20
- from auto_rest.query_params import get_pagination_params, apply_pagination_params
18
+ from auto_rest.query_params import create_pagination_dependency, apply_pagination_params
21
19
 
22
20
  app = FastAPI()
23
21
 
24
22
  @app.get("/items/")
25
23
  async def list_items(
26
- pagination_params: dict = Depends(get_pagination_params),
24
+ pagination_params: dict = create_pagination_dependency(model),
27
25
  response: Response
28
26
  ):
29
- query = select(SomeModel)
27
+ query = select(model)
30
28
  query = apply_pagination_params(query, pagination_params, response)
31
29
  return ... # Logic to further process and execute the query goes here
32
30
  ```
33
31
  """
34
32
 
35
- from typing import Literal
33
+ from collections.abc import Callable
34
+ from typing import Literal, Optional
36
35
 
37
- from fastapi import Query
38
- from sqlalchemy import asc, desc
36
+ from fastapi import Depends, Query
37
+ from sqlalchemy import asc, desc, Table
39
38
  from sqlalchemy.sql.selectable import Select
40
39
  from starlette.responses import Response
41
40
 
42
41
  __all__ = [
43
42
  "apply_ordering_params",
44
43
  "apply_pagination_params",
45
- "get_ordering_params",
46
- "get_pagination_params",
44
+ "create_ordering_dependency",
45
+ "create_pagination_dependency",
47
46
  ]
48
47
 
49
48
 
50
- def get_pagination_params(
51
- _limit_: int = Query(10, ge=0, description="The maximum number of records to return."),
52
- _offset_: int = Query(0, ge=0, description="The starting index of the returned records."),
53
- ) -> dict[str, int]:
54
- """Extract pagination parameters from request query parameters.
49
+ def create_ordering_dependency(table: Table) -> Callable[..., dict]:
50
+ """Create an injectable dependency for fetching ordering arguments from query parameters.
55
51
 
56
52
  Args:
57
- _limit_: The maximum number of records to return.
58
- _offset_: The starting index of the returned records.
53
+ table: The database table to create the dependency for.
59
54
 
60
55
  Returns:
61
- dict: A dictionary containing the `limit` and `offset` values.
56
+ An injectable FastAPI dependency.
62
57
  """
63
58
 
64
- return {"limit": _limit_, "offset": _offset_}
59
+ columns = tuple(table.columns.keys())
65
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
66
 
67
- def apply_pagination_params(query: Select, params: dict[str, int], response: Response) -> Select:
68
- """Apply pagination to a database query.
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)
69
78
 
70
- Returns a copy of the provided query with offset and limit parameters applied.
71
- Compatible with parameters returned by the `get_pagination_params` method.
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.
72
86
 
73
87
  Args:
74
88
  query: The database query to apply parameters to.
@@ -76,47 +90,64 @@ def apply_pagination_params(query: Select, params: dict[str, int], response: Res
76
90
  response: The outgoing HTTP response object.
77
91
 
78
92
  Returns:
79
- A copy of the query modified to only return the paginated values.
93
+ A copy of the query modified to return ordered values.
80
94
  """
81
95
 
82
- limit = params.get("limit", 0)
83
- offset = params.get("offset", 0)
96
+ order_by = params.get("order_by")
97
+ direction = params.get("direction")
84
98
 
85
- if limit < 0 or offset < 0:
86
- raise ValueError("Pagination parameters must be greater than or equal to zero.")
99
+ # Set common response headers
100
+ response.headers["X-Order-By"] = str(order_by)
101
+ response.headers["X-Order-Direction"] = str(direction)
87
102
 
88
- if limit == 0:
89
- response.headers["X-Pagination-Applied"] = "false"
103
+ if order_by is None:
104
+ response.headers["X-Order-Applied"] = "false"
90
105
  return query
91
106
 
92
- response.headers["X-Pagination-Applied"] = "true"
93
- response.headers["X-Pagination-Limit"] = str(limit)
94
- response.headers["X-Pagination-Offset"] = str(offset)
95
- return query.offset(offset).limit(limit)
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))
96
114
 
97
115
 
98
- def get_ordering_params(
99
- _order_by_: str | None = Query(None, description="The field name to sort by."),
100
- _direction_: Literal["asc", "desc"] = Query("asc", description="Sort results in 'asc' or 'desc' order.")
101
- ) -> dict:
102
- """Extract ordering parameters from request query parameters.
116
+ def create_pagination_dependency(table: Table) -> Callable[..., dict]:
117
+ """Create an injectable dependency for fetching pagination arguments from query parameters.
103
118
 
104
119
  Args:
105
- _order_by_: The field to order by.
106
- _direction_: The direction to order by.
120
+ table: The database table to create the dependency for.
107
121
 
108
122
  Returns:
109
- dict: A dictionary containing the `order_by` and `direction` values.
123
+ An injectable FastAPI dependency.
110
124
  """
111
125
 
112
- return {"order_by": _order_by_, "direction": _direction_}
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.
113
131
 
132
+ Args:
133
+ _limit_: The maximum number of records to return.
134
+ _offset_: The starting index of the returned records.
114
135
 
115
- def apply_ordering_params(query: Select, params: dict, response: Response) -> Select:
116
- """Apply ordering to a database query.
136
+ Returns:
137
+ dict: A dictionary containing the `limit` and `offset` values.
138
+ """
117
139
 
118
- Returns a copy of the provided query with ordering parameters applied.
119
- Compatible with parameters returned by the `get_ordering_params` method.
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.
120
151
 
121
152
  Args:
122
153
  query: The database query to apply parameters to.
@@ -124,24 +155,20 @@ def apply_ordering_params(query: Select, params: dict, response: Response) -> Se
124
155
  response: The outgoing HTTP response object.
125
156
 
126
157
  Returns:
127
- A copy of the query modified to return ordered values.
158
+ A copy of the query modified to only return the paginated values.
128
159
  """
129
160
 
130
- order_by = params.get("order_by")
131
- direction = params.get("direction", "asc")
161
+ limit = params.get("limit")
162
+ offset = params.get("offset")
132
163
 
133
- if not order_by:
134
- response.headers["X-Order-Applied"] = "false"
135
- return query
136
-
137
- response.headers["X-Order-Applied"] = "true"
138
- response.headers["X-Order-By"] = order_by
139
- response.headers["X-Order-Direction"] = direction
140
-
141
- if direction == "asc":
142
- return query.order_by(asc(order_by))
164
+ # Set common response headers
165
+ response.headers["X-Pagination-Limit"] = str(limit)
166
+ response.headers["X-Pagination-Offset"] = str(offset)
143
167
 
144
- if direction == "desc":
145
- return query.order_by(desc(order_by))
168
+ # Do not apply pagination if not requested
169
+ if limit in (0, None):
170
+ response.headers["X-Pagination-Applied"] = "false"
171
+ return query
146
172
 
147
- raise ValueError("Ordering direction must be 'asc' or 'desc'.")
173
+ response.headers["X-Pagination-Applied"] = "true"
174
+ return query.offset(offset or 0).limit(limit)
auto_rest/queries.py CHANGED
@@ -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")
auto_rest/routers.py CHANGED
@@ -24,15 +24,15 @@ routers to be added directly to an API application instance.
24
24
  """
25
25
 
26
26
  from fastapi import APIRouter
27
- from sqlalchemy import MetaData
27
+ from sqlalchemy import MetaData, Table
28
28
  from starlette import status
29
29
 
30
30
  from auto_rest.handlers import *
31
- from auto_rest.models import DBEngine, DBModel
31
+ from auto_rest.models import DBEngine
32
32
 
33
33
  __all__ = [
34
34
  "create_meta_router",
35
- "create_model_router",
35
+ "create_table_router",
36
36
  "create_welcome_router",
37
37
  ]
38
38
 
@@ -45,7 +45,13 @@ def create_welcome_router() -> APIRouter:
45
45
  """
46
46
 
47
47
  router = APIRouter()
48
- router.add_api_route("/", create_welcome_handler(), methods=["GET"], include_in_schema=False)
48
+ router.add_api_route(
49
+ path="/",
50
+ methods=["GET"],
51
+ endpoint=create_welcome_handler(),
52
+ include_in_schema=False
53
+ )
54
+
49
55
  return router
50
56
 
51
57
 
@@ -58,7 +64,7 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
58
64
  Args:
59
65
  engine: The database engine used to facilitate database interactions.
60
66
  metadata: The metadata object containing the database schema.
61
- version: The application name.
67
+ name: The application name.
62
68
  version: The application versionnumber.
63
69
 
64
70
  Returns:
@@ -68,101 +74,110 @@ def create_meta_router(engine: DBEngine, metadata: MetaData, name: str, version:
68
74
  router = APIRouter()
69
75
  tags = ["Application Metadata"]
70
76
 
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)
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
+
74
101
  return router
75
102
 
76
103
 
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.
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.
79
106
 
80
107
  Args:
81
108
  engine: The SQLAlchemy engine connected to the database.
82
- model: The ORM model class representing a database table.
109
+ table: The database table to create API endpoints for.
83
110
  writeable: Whether the router should include support for write operations.
84
111
 
85
112
  Returns:
86
- An APIRouter instance with routes for database operations on the model.
113
+ An APIRouter instance with routes for database operations on the table.
87
114
  """
88
115
 
89
116
  router = APIRouter()
90
117
 
91
118
  # Construct path parameters from primary key columns
92
- pk_columns = model.__table__.primary_key.columns
93
- path_params_url = "/".join(f"{{{col.name}}}" for col in pk_columns)
94
- path_params_openapi = {
95
- "parameters": [
96
- {
97
- "name": col.name,
98
- "in": "path",
99
- "required": True
100
- } for col in pk_columns
101
- ]
102
- }
103
-
104
- # Raise an error if no primary key columns are found
105
- # (SQLAlchemy should ensure this never happens)
106
- if not pk_columns: # pragma: no cover
107
- raise RuntimeError(f"No primary key columns found for table {model.__tablename__}.")
108
-
109
- # Define routes for read operations
110
- router.add_api_route(
111
- path="/",
112
- methods=["GET"],
113
- endpoint=create_list_records_handler(engine, model),
114
- status_code=status.HTTP_200_OK,
115
- tags=[model.__name__],
116
- openapi_extra=path_params_openapi
117
- )
118
-
119
- router.add_api_route(
120
- path=f"/{path_params_url}/",
121
- methods=["GET"],
122
- endpoint=create_get_record_handler(engine, model),
123
- status_code=status.HTTP_200_OK,
124
- tags=[model.__name__],
125
- openapi_extra=path_params_openapi
126
- )
127
-
128
- if not writeable:
129
- return router
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)
130
121
 
131
- # Define routes for write operations
122
+ # Add route for read operations against the table
132
123
  router.add_api_route(
133
124
  path="/",
134
- methods=["POST"],
135
- endpoint=create_post_record_handler(engine, model),
136
- status_code=status.HTTP_201_CREATED,
137
- tags=[model.__name__],
138
- openapi_extra=path_params_openapi
139
- )
140
-
141
- router.add_api_route(
142
- path=f"/{path_params_url}/",
143
- methods=["PUT"],
144
- endpoint=create_put_record_handler(engine, model),
145
- status_code=status.HTTP_200_OK,
146
- tags=[model.__name__],
147
- openapi_extra=path_params_openapi
148
- )
149
-
150
- router.add_api_route(
151
- path=f"/{path_params_url}/",
152
- methods=["PATCH"],
153
- endpoint=create_patch_record_handler(engine, model),
125
+ methods=["GET"],
126
+ endpoint=create_list_records_handler(engine, table),
154
127
  status_code=status.HTTP_200_OK,
155
- tags=[model.__name__],
156
- openapi_extra=path_params_openapi
128
+ summary="Fetch multiple records from the table.",
129
+ tags=[table.name],
157
130
  )
158
131
 
159
- router.add_api_route(
160
- path=f"/{path_params_url}/",
161
- methods=["DELETE"],
162
- endpoint=create_delete_record_handler(engine, model),
163
- status_code=status.HTTP_200_OK,
164
- tags=[model.__name__],
165
- openapi_extra=path_params_openapi
166
- )
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
+ )
167
182
 
168
183
  return router
@@ -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
@@ -0,0 +1,15 @@
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,,
@@ -1,13 +0,0 @@
1
- auto_rest/__init__.py,sha256=9ICmv2urSoAo856FJylKdorF19UsUGc4eyORYLptf1Q,69
2
- auto_rest/__main__.py,sha256=RK3SuyGrWmXAjWZfZZJs-FP9srqjgdzhm86MuBucOok,3197
3
- auto_rest/cli.py,sha256=A7-kMTNNzqZB7jTUJBAVqfYm9RyYjENr0g_vK9JE0N4,5199
4
- auto_rest/handlers.py,sha256=0nj3radoT0V2bCSmb5gBuWyu1CtUgSQ_NU0typ8zKBA,11827
5
- auto_rest/models.py,sha256=XfzBdfnjQCXmcQBOYsC7OsZ69EBCKlANrcPnO5Tj8fQ,6882
6
- auto_rest/params.py,sha256=f5gIeCIlzvy70IgtfJ96Krohf44AySwR5k-b1DYeIXw,4953
7
- auto_rest/queries.py,sha256=GzoCtxRPn9UJ7cnhYuPc8wZFppbqe7ykSlnesfWSr94,2926
8
- auto_rest/routers.py,sha256=VzGPEwJe8GqLly751EWIru_LwjB7R9t1z9OPoYitc9I,5431
9
- auto_rest_api-0.1.0.dist-info/LICENSE.md,sha256=zFRw_u1mGSOH8GrpOu0L1P765aX9fB5UpKz06mTxAos,34893
10
- auto_rest_api-0.1.0.dist-info/METADATA,sha256=tvb1bdogwjEIf8e6XQYuvRY65ZCfrKNInbQBKmkR8cc,2951
11
- auto_rest_api-0.1.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
12
- auto_rest_api-0.1.0.dist-info/entry_points.txt,sha256=zFynmBrHyYo3Ds0Uo4-bTFe1Tdr5mIXV4dPQOFb-W1w,53
13
- auto_rest_api-0.1.0.dist-info/RECORD,,