Fast-Controller 0.0.0.dev0__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.
- fast_controller-0.0.0.dev0/PKG-INFO +55 -0
- fast_controller-0.0.0.dev0/README.md +30 -0
- fast_controller-0.0.0.dev0/fast_controller/__init__.py +237 -0
- fast_controller-0.0.0.dev0/fast_controller/resource.py +80 -0
- fast_controller-0.0.0.dev0/fast_controller/util.py +19 -0
- fast_controller-0.0.0.dev0/pyproject.toml +58 -0
- fast_controller-0.0.0.dev0/tests/test_controller.py +0 -0
- fast_controller-0.0.0.dev0/tests/test_resource.py +0 -0
- fast_controller-0.0.0.dev0/tests/test_util.py +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: Fast-Controller
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Summary: The fastest way to a turn your models into a full ReST API
|
|
5
|
+
Keywords: controller,base,rest,api,backend
|
|
6
|
+
Author-Email: Cody M Sommer <bassmastacod@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Project-URL: Repository, https://github.com/BassMastaCod/Fast-Controller.git
|
|
22
|
+
Project-URL: Issues, https://github.com/BassMastaCod/Fast-Controller/issues
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Fast-Controller
|
|
27
|
+
A fast solution to creating a ReST backend for your Python models.
|
|
28
|
+
|
|
29
|
+
Turn your models into _Resources_ and give them a controller layer.
|
|
30
|
+
Provides standard functionality with limited effort using
|
|
31
|
+
[DAOModel](https://pypi.org/project/DAOModel/)
|
|
32
|
+
and [FastAPI](https://fastapi.tiangolo.com/).
|
|
33
|
+
|
|
34
|
+
## Supported Actions
|
|
35
|
+
* `search`
|
|
36
|
+
* `create`
|
|
37
|
+
* `upsert`
|
|
38
|
+
* `view`
|
|
39
|
+
* `rename`
|
|
40
|
+
* `modify`
|
|
41
|
+
* `delete`
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
* Expandable controllers so you can add endpoints for additional functionality
|
|
45
|
+
* Built-in validation
|
|
46
|
+
* Ability to pick and choose which actions to support for each resource
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
## Additional Functionality
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
## Caveats
|
|
55
|
+
...
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Fast-Controller
|
|
2
|
+
A fast solution to creating a ReST backend for your Python models.
|
|
3
|
+
|
|
4
|
+
Turn your models into _Resources_ and give them a controller layer.
|
|
5
|
+
Provides standard functionality with limited effort using
|
|
6
|
+
[DAOModel](https://pypi.org/project/DAOModel/)
|
|
7
|
+
and [FastAPI](https://fastapi.tiangolo.com/).
|
|
8
|
+
|
|
9
|
+
## Supported Actions
|
|
10
|
+
* `search`
|
|
11
|
+
* `create`
|
|
12
|
+
* `upsert`
|
|
13
|
+
* `view`
|
|
14
|
+
* `rename`
|
|
15
|
+
* `modify`
|
|
16
|
+
* `delete`
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
* Expandable controllers so you can add endpoints for additional functionality
|
|
20
|
+
* Built-in validation
|
|
21
|
+
* Ability to pick and choose which actions to support for each resource
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
## Additional Functionality
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
## Caveats
|
|
30
|
+
...
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from typing import Optional, Callable, Annotated
|
|
3
|
+
|
|
4
|
+
from daomodel import DAOModel
|
|
5
|
+
from daomodel.dao import NotFound
|
|
6
|
+
from daomodel.db import create_engine, DAOFactory
|
|
7
|
+
from daomodel.util import names_of
|
|
8
|
+
from fastapi import FastAPI, APIRouter, Depends, Path, Body, Query
|
|
9
|
+
from fastapi.openapi.models import Response
|
|
10
|
+
|
|
11
|
+
from fast_controller.resource import Resource
|
|
12
|
+
from fast_controller.util import docstring_format, paginated
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SessionLocal = create_engine()
|
|
16
|
+
def get_daos() -> DAOFactory:
|
|
17
|
+
"""Yields a DAOFactory."""
|
|
18
|
+
with DAOFactory(SessionLocal) as daos:
|
|
19
|
+
yield daos
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Action(Enum):
|
|
23
|
+
VIEW = auto()
|
|
24
|
+
SEARCH = auto()
|
|
25
|
+
CREATE = auto()
|
|
26
|
+
UPSERT = auto()
|
|
27
|
+
MODIFY = auto()
|
|
28
|
+
RENAME = auto()
|
|
29
|
+
DELETE = auto()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Controller:
|
|
33
|
+
def __init__(self, app: Optional[FastAPI] = None):
|
|
34
|
+
self.app = app
|
|
35
|
+
|
|
36
|
+
def init_app(self, app: FastAPI) -> None:
|
|
37
|
+
self.app = app
|
|
38
|
+
|
|
39
|
+
# todo : define both ways to define apis
|
|
40
|
+
@classmethod
|
|
41
|
+
def create_api_group(cls,
|
|
42
|
+
resource: Optional[type[Resource]] = None,
|
|
43
|
+
prefix: Optional[str] = None,
|
|
44
|
+
skip: Optional[set[Action]] = None) -> APIRouter:
|
|
45
|
+
api_router = APIRouter(prefix=prefix if prefix else resource.get_path())
|
|
46
|
+
if resource:
|
|
47
|
+
cls.__register_resource_endpoints(api_router, resource, skip)
|
|
48
|
+
return api_router
|
|
49
|
+
|
|
50
|
+
def register_resource(self,
|
|
51
|
+
resource: type[Resource],
|
|
52
|
+
skip: Optional[set[Action]] = None,
|
|
53
|
+
additional_endpoints: Optional[Callable] = None) -> None:
|
|
54
|
+
if skip is None:
|
|
55
|
+
skip = set()
|
|
56
|
+
api_router = APIRouter(prefix=resource.get_path(), tags=[resource.doc_name()])
|
|
57
|
+
self.__register_resource_endpoints(api_router, resource, skip)
|
|
58
|
+
if additional_endpoints:
|
|
59
|
+
additional_endpoints(api_router)
|
|
60
|
+
self.app.include_router(api_router)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def __register_resource_endpoints(cls, router: APIRouter, resource: type[Resource], skip) -> None:
|
|
64
|
+
if Action.SEARCH not in skip:
|
|
65
|
+
@router.get("/", response_model=list[resource.get_output_schema()])
|
|
66
|
+
@docstring_format(resource=resource.doc_name())
|
|
67
|
+
def search(response: Response,
|
|
68
|
+
filters: Annotated[paginated(resource.get_search_schema()), Query()],
|
|
69
|
+
daos: DAOFactory = Depends(get_daos)) -> list[DAOModel]:
|
|
70
|
+
"""Searches for {resource} by criteria"""
|
|
71
|
+
results = daos[resource].find(**filters.model_dump())
|
|
72
|
+
response.headers["x-total-count"] = str(results.total)
|
|
73
|
+
response.headers["x-page"] = str(results.page)
|
|
74
|
+
response.headers["x-per-page"] = str(results.per_page)
|
|
75
|
+
return results
|
|
76
|
+
|
|
77
|
+
if Action.CREATE not in skip:
|
|
78
|
+
@router.post("/", response_model=resource.get_detailed_output_schema(), status_code=201)
|
|
79
|
+
@docstring_format(resource=resource.doc_name())
|
|
80
|
+
def create(data: Annotated[resource.get_input_schema(), Query()],
|
|
81
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
82
|
+
"""Creates a new {resource}"""
|
|
83
|
+
return daos[resource].create_with(**data.model_dump())
|
|
84
|
+
|
|
85
|
+
if Action.UPSERT not in skip:
|
|
86
|
+
@router.put("/", response_model=resource.get_detailed_output_schema())
|
|
87
|
+
@docstring_format(resource=resource.doc_name())
|
|
88
|
+
def upsert(data: Annotated[resource.get_input_schema(), Query()],
|
|
89
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
90
|
+
"""Creates/modifies a {resource}"""
|
|
91
|
+
return daos[resource].upsert(**data.model_dump())
|
|
92
|
+
|
|
93
|
+
pk = [p.name for p in resource.get_pk()]
|
|
94
|
+
path = "/".join([""] + ["{" + p + "}" for p in pk])
|
|
95
|
+
|
|
96
|
+
# Caveat: Only up to 5 columns are supported within a primary key.
|
|
97
|
+
# This allows us to avoid resorting to exec() while **kwargs is unsupported for Path variables
|
|
98
|
+
match len(pk):
|
|
99
|
+
case 1:
|
|
100
|
+
if Action.VIEW not in skip:
|
|
101
|
+
@router.get(path, response_model=resource.get_detailed_output_schema())
|
|
102
|
+
@docstring_format(resource=resource.doc_name())
|
|
103
|
+
def view(pk0 = Path(alias=pk[0]),
|
|
104
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
105
|
+
"""Retrieves a detailed view of a {resource}"""
|
|
106
|
+
return daos[resource].get(pk0)
|
|
107
|
+
|
|
108
|
+
# Caveat: Rename action is only supported for resources with a single column primary key
|
|
109
|
+
if Action.RENAME not in skip:
|
|
110
|
+
@router.post(path, response_model=resource.get_detailed_output_schema())
|
|
111
|
+
@docstring_format(resource=resource.doc_name())
|
|
112
|
+
def rename(pk0 = Path(alias=pk[0]),
|
|
113
|
+
new_id = Body(alias=pk[0]),
|
|
114
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
115
|
+
"""Renames a {resource}"""
|
|
116
|
+
current = daos[resource].get(pk0)
|
|
117
|
+
try:
|
|
118
|
+
existing = daos[resource].get(new_id)
|
|
119
|
+
# todo Set FKs
|
|
120
|
+
for fk in resource.get_fks():
|
|
121
|
+
fk = new_id
|
|
122
|
+
daos[resource].remove_model(current, commit=False)
|
|
123
|
+
current = existing
|
|
124
|
+
except NotFound:
|
|
125
|
+
for p in names_of(current.get_pk()):
|
|
126
|
+
setattr(current, p, new_id)
|
|
127
|
+
# todo Set entire PK
|
|
128
|
+
return daos[resource].update_model(current)
|
|
129
|
+
|
|
130
|
+
# Caveat: Modify action is only supported for resources with a single column primary key
|
|
131
|
+
# Use Upsert instead for multi-column PK resources
|
|
132
|
+
if Action.MODIFY not in skip:
|
|
133
|
+
@router.put(path, response_model=resource.get_detailed_output_schema())
|
|
134
|
+
@docstring_format(resource=resource.doc_name())
|
|
135
|
+
def update(data: Annotated[resource.get_update_schema(), Body()], # TODO - Remove PK from input schema
|
|
136
|
+
pk0 = Path(alias=pk[0]),
|
|
137
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
138
|
+
"""Creates/modifies a {resource}"""
|
|
139
|
+
result = daos[resource].get(pk0)
|
|
140
|
+
result.copy_values(**data.model_dump())
|
|
141
|
+
return daos[resource].update(**result.model_dump())
|
|
142
|
+
|
|
143
|
+
if Action.DELETE not in skip:
|
|
144
|
+
@router.delete(path, status_code=204)
|
|
145
|
+
@docstring_format(resource=resource.doc_name())
|
|
146
|
+
def delete(pk0 = Path(alias=pk[0]),
|
|
147
|
+
daos: DAOFactory = Depends(get_daos)) -> None:
|
|
148
|
+
"""Deletes a {resource}"""
|
|
149
|
+
daos[resource].remove(pk0)
|
|
150
|
+
|
|
151
|
+
case 2:
|
|
152
|
+
if Action.VIEW not in skip:
|
|
153
|
+
@router.get(path, response_model=resource.get_detailed_output_schema())
|
|
154
|
+
@docstring_format(resource=resource.doc_name())
|
|
155
|
+
def view(pk0 = Path(alias=pk[0]),
|
|
156
|
+
pk1 = Path(alias=pk[1]),
|
|
157
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
158
|
+
"""Retrieves a detailed view of a {resource}"""
|
|
159
|
+
return daos[resource].get(pk0, pk1)
|
|
160
|
+
|
|
161
|
+
if Action.DELETE not in skip:
|
|
162
|
+
@router.delete(path, status_code=204)
|
|
163
|
+
@docstring_format(resource=resource.doc_name())
|
|
164
|
+
def delete(pk0 = Path(alias=pk[0]),
|
|
165
|
+
pk1 = Path(alias=pk[1]),
|
|
166
|
+
daos: DAOFactory = Depends(get_daos)) -> None:
|
|
167
|
+
"""Deletes a {resource}"""
|
|
168
|
+
daos[resource].remove(pk0, pk1)
|
|
169
|
+
|
|
170
|
+
case 3:
|
|
171
|
+
if Action.VIEW not in skip:
|
|
172
|
+
@router.get(path, response_model=resource.get_detailed_output_schema())
|
|
173
|
+
@docstring_format(resource=resource.doc_name())
|
|
174
|
+
def view(pk0 = Path(alias=pk[0]),
|
|
175
|
+
pk1 = Path(alias=pk[1]),
|
|
176
|
+
pk2 = Path(alias=pk[2]),
|
|
177
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
178
|
+
"""Retrieves a detailed view of a {resource}"""
|
|
179
|
+
return daos[resource].get(pk0, pk1, pk2)
|
|
180
|
+
|
|
181
|
+
if Action.DELETE not in skip:
|
|
182
|
+
@router.delete(path, status_code=204)
|
|
183
|
+
@docstring_format(resource=resource.doc_name())
|
|
184
|
+
def delete(pk0 = Path(alias=pk[0]),
|
|
185
|
+
pk1 = Path(alias=pk[1]),
|
|
186
|
+
pk2 = Path(alias=pk[2]),
|
|
187
|
+
daos: DAOFactory = Depends(get_daos)) -> None:
|
|
188
|
+
"""Deletes a {resource}"""
|
|
189
|
+
daos[resource].remove(pk0, pk1, pk2)
|
|
190
|
+
|
|
191
|
+
case 4:
|
|
192
|
+
if Action.VIEW not in skip:
|
|
193
|
+
@router.get(path, response_model=resource.get_detailed_output_schema())
|
|
194
|
+
@docstring_format(resource=resource.doc_name())
|
|
195
|
+
def view(pk0 = Path(alias=pk[0]),
|
|
196
|
+
pk1 = Path(alias=pk[1]),
|
|
197
|
+
pk2 = Path(alias=pk[2]),
|
|
198
|
+
pk3 = Path(alias=pk[3]),
|
|
199
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
200
|
+
"""Retrieves a detailed view of a {resource}"""
|
|
201
|
+
return daos[resource].get(pk0, pk1, pk2, pk3)
|
|
202
|
+
|
|
203
|
+
if Action.DELETE not in skip:
|
|
204
|
+
@router.delete(path, status_code=204)
|
|
205
|
+
@docstring_format(resource=resource.doc_name())
|
|
206
|
+
def delete(pk0 = Path(alias=pk[0]),
|
|
207
|
+
pk1 = Path(alias=pk[1]),
|
|
208
|
+
pk2 = Path(alias=pk[2]),
|
|
209
|
+
pk3 = Path(alias=pk[3]),
|
|
210
|
+
daos: DAOFactory = Depends(get_daos)) -> None:
|
|
211
|
+
"""Deletes a {resource}"""
|
|
212
|
+
daos[resource].remove(pk0, pk1, pk2, pk3)
|
|
213
|
+
|
|
214
|
+
case 5:
|
|
215
|
+
if Action.VIEW not in skip:
|
|
216
|
+
@router.get(path, response_model=resource.get_detailed_output_schema())
|
|
217
|
+
@docstring_format(resource=resource.doc_name())
|
|
218
|
+
def view(pk0 = Path(alias=pk[0]),
|
|
219
|
+
pk1 = Path(alias=pk[1]),
|
|
220
|
+
pk2 = Path(alias=pk[2]),
|
|
221
|
+
pk3 = Path(alias=pk[3]),
|
|
222
|
+
pk4 = Path(alias=pk[4]),
|
|
223
|
+
daos: DAOFactory = Depends(get_daos)) -> DAOModel:
|
|
224
|
+
"""Retrieves a detailed view of a {resource}"""
|
|
225
|
+
return daos[resource].get(pk0, pk1, pk2, pk3, pk4)
|
|
226
|
+
|
|
227
|
+
if Action.DELETE not in skip:
|
|
228
|
+
@router.delete(path, status_code=204)
|
|
229
|
+
@docstring_format(resource=resource.doc_name())
|
|
230
|
+
def delete(pk0 = Path(alias=pk[0]),
|
|
231
|
+
pk1 = Path(alias=pk[1]),
|
|
232
|
+
pk2 = Path(alias=pk[2]),
|
|
233
|
+
pk3 = Path(alias=pk[3]),
|
|
234
|
+
pk4 = Path(alias=pk[4]),
|
|
235
|
+
daos: DAOFactory = Depends(get_daos)) -> None:
|
|
236
|
+
"""Deletes a {resource}"""
|
|
237
|
+
daos[resource].remove(pk0, pk1, pk2, pk3, pk4)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from inspect import isclass
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
from daomodel import DAOModel
|
|
5
|
+
from sqlmodel import SQLModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def either(preferred, default):
|
|
9
|
+
return preferred if isclass(preferred) and issubclass(preferred, SQLModel) else default
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Resource(DAOModel):
|
|
13
|
+
__abstract__ = True
|
|
14
|
+
_default_schema: type[SQLModel]
|
|
15
|
+
_search_schema: type[SQLModel]
|
|
16
|
+
_input_schema: type[SQLModel]
|
|
17
|
+
_update_schema: type[SQLModel]
|
|
18
|
+
_output_schema: type[SQLModel]
|
|
19
|
+
_detailed_output_schema: type[SQLModel]
|
|
20
|
+
path: ClassVar[str]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_path(cls) -> str:
|
|
24
|
+
return getattr(cls, "path", "/api/" + cls.normalized_name())
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def validate(cls, column_name, value):
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_base(cls) -> type[SQLModel]:
|
|
32
|
+
return cls
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def set_default_schema(cls, schema: type[SQLModel]) -> None:
|
|
36
|
+
cls._default_schema = schema
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_default_schema(cls) -> type[SQLModel]:
|
|
40
|
+
return cls._default_schema
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def set_search_schema(cls, schema: type[SQLModel]) -> None:
|
|
44
|
+
cls._search_schema = schema
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_search_schema(cls) -> type[SQLModel]:
|
|
48
|
+
return either(cls._search_schema, cls.get_default_schema())
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def set_input_schema(cls, schema: type[SQLModel]) -> None:
|
|
52
|
+
cls._input_schema = schema
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_input_schema(cls) -> type[SQLModel]:
|
|
56
|
+
return either(cls._input_schema, cls.get_default_schema())
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def set_update_schema(cls, schema: type[SQLModel]) -> None:
|
|
60
|
+
cls._update_schema = schema
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get_update_schema(cls) -> type[SQLModel]:
|
|
64
|
+
return either(cls._update_schema, cls.get_default_schema())
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def set_output_schema(cls, schema: type[SQLModel]) -> None:
|
|
68
|
+
cls._output_schema = schema
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_output_schema(cls) -> type[SQLModel]:
|
|
72
|
+
return either(cls._output_schema, cls.get_default_schema())
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def set_detailed_output_schema(cls, schema: type[SQLModel]) -> None:
|
|
76
|
+
cls._detailed_output_schema = schema
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def get_detailed_output_schema(cls) -> type[SQLModel]:
|
|
80
|
+
return either(cls._detailed_output_schema, cls.get_output_schema())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
|
|
3
|
+
from sqlmodel import Field, SQLModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def docstring_format(**kwargs):
|
|
7
|
+
def decorator(func: Callable):
|
|
8
|
+
func.__doc__ = func.__doc__.format(**kwargs)
|
|
9
|
+
return func
|
|
10
|
+
return decorator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def paginated(superclass: type[SQLModel]):
|
|
14
|
+
class OptionalPK(superclass, table=True): # table=True is a hack to make PK optional
|
|
15
|
+
pass
|
|
16
|
+
class Paginated(OptionalPK):
|
|
17
|
+
page: Optional[int] = Field(default=None, gt=0)
|
|
18
|
+
per_page: Optional[int] = Field(default=None, gt=0)
|
|
19
|
+
return Paginated
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"pdm-backend",
|
|
4
|
+
"fastapi",
|
|
5
|
+
"daomodel",
|
|
6
|
+
"SQLModel",
|
|
7
|
+
]
|
|
8
|
+
build-backend = "pdm.backend"
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "Fast-Controller"
|
|
12
|
+
dynamic = []
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Cody M Sommer", email = "bassmastacod@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
description = "The fastest way to a turn your models into a full ReST API"
|
|
17
|
+
keywords = [
|
|
18
|
+
"controller",
|
|
19
|
+
"base",
|
|
20
|
+
"rest",
|
|
21
|
+
"api",
|
|
22
|
+
"backend",
|
|
23
|
+
]
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
requires-python = ">=3.7"
|
|
26
|
+
dependencies = []
|
|
27
|
+
classifiers = [
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3.7",
|
|
30
|
+
"Programming Language :: Python :: 3.8",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
38
|
+
"Intended Audience :: Developers",
|
|
39
|
+
"Topic :: Software Development :: Libraries",
|
|
40
|
+
"Typing :: Typed",
|
|
41
|
+
]
|
|
42
|
+
version = "0.0.0.dev0"
|
|
43
|
+
|
|
44
|
+
[project.license]
|
|
45
|
+
text = "MIT"
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Repository = "https://github.com/BassMastaCod/Fast-Controller.git"
|
|
49
|
+
Issues = "https://github.com/BassMastaCod/Fast-Controller/issues"
|
|
50
|
+
|
|
51
|
+
[tool.pdm.version]
|
|
52
|
+
source = "scm"
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
pythonpath = "fast_controller"
|
|
56
|
+
addopts = [
|
|
57
|
+
"--import-mode=importlib",
|
|
58
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|