fastapi-crudrouter-v2 0.1.3__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.
- fastapi_crudrouter_v2-0.1.3/LICENSE +21 -0
- fastapi_crudrouter_v2-0.1.3/PKG-INFO +106 -0
- fastapi_crudrouter_v2-0.1.3/README.md +88 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/__init__.py +9 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/_version.py +1 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/core/__init__.py +4 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/core/_base.py +212 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/core/_router.py +114 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/core/_types.py +12 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/core/_utils.py +74 -0
- fastapi_crudrouter_v2-0.1.3/fastapi_crudrouter_v2/py.typed +0 -0
- fastapi_crudrouter_v2-0.1.3/pyproject.toml +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Adam Watkins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-crudrouter-v2
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A dynamic FastAPI router that automatically creates CRUD routes for your models
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: pasha_danilevich
|
|
7
|
+
Author-email: danilevitch.pasha@yandex.ru
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/master/docs/en/docs/assets/logo.png" height="200" />
|
|
20
|
+
</p>
|
|
21
|
+
<p align="center">
|
|
22
|
+
<em>⚡ Create CRUD routes with lighting speed</em> ⚡</br>
|
|
23
|
+
<sub>A dynamic FastAPI router that automatically creates CRUD routes for your models</sub>
|
|
24
|
+
</p>
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/awtkns/fastapi-crudrouter/.github/workflows/pytest.yml?color=%2334D058" />
|
|
27
|
+
<img alt="Downloads" src="https://img.shields.io/pypi/dm/fastapi-crudrouter?color=%2334D058" />
|
|
28
|
+
<a href="https://pypi.org/project/fastapi-crudrouter" target="_blank">
|
|
29
|
+
<img src="https://img.shields.io/pypi/v/fastapi-crudrouter?color=%2334D058&label=pypi%20package" alt="Package version">
|
|
30
|
+
</a>
|
|
31
|
+
<img alt="License" src="https://img.shields.io/github/license/awtkns/fastapi-crudrouter?color=%2334D058" />
|
|
32
|
+
</p>
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/fastapi-crudrouter">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
**Documentation**: <a href="https://fastapi-crudrouter.awtkns.com" target="_blank">https://fastapi-crudrouter.awtkns.com</a>
|
|
40
|
+
|
|
41
|
+
**Source Code**: <a href="https://github.com/awtkns/fastapi-crudrouter" target="_blank">https://github.com/awtkns/fastapi-crudrouter</a>
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
Tired of rewriting generic CRUD routes? Need to rapidly prototype a feature for a presentation
|
|
45
|
+
or a hackathon? Thankfully, [fastapi-crudrouter](https://github.com/awtkns/fastapi-crudrouter) has your back. As an
|
|
46
|
+
extension to the APIRouter included with [FastAPI](https://fastapi.tiangolo.com/), the FastAPI CRUDRouter will automatically
|
|
47
|
+
generate and document your CRUD routes for you, all you have to do is pass your model and maybe your database connection.
|
|
48
|
+
|
|
49
|
+
FastAPI-CRUDRouter is **lighting fast**, well tested, and **production ready**.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
```bash
|
|
54
|
+
pip install fastapi-crudrouter
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Basic Usage
|
|
58
|
+
Below is a simple example of what the CRUDRouter can do. In just ten lines of code, you can generate all
|
|
59
|
+
the crud routes you need for any model. A full list of the routes generated can be found [here](https://fastapi-crudrouter.awtkns.com/routing).
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pydantic import BaseModel
|
|
63
|
+
from fastapi import FastAPI
|
|
64
|
+
from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
|
|
65
|
+
|
|
66
|
+
class Potato(BaseModel):
|
|
67
|
+
id: int
|
|
68
|
+
color: str
|
|
69
|
+
mass: float
|
|
70
|
+
|
|
71
|
+
app = FastAPI()
|
|
72
|
+
app.include_router(CRUDRouter(schema=Potato))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Advanced Usage
|
|
76
|
+
fastapi-crudrouter provides a number of features that allow you to get the most out of your automatically generated CRUD
|
|
77
|
+
routes. Listed below are some highlights.
|
|
78
|
+
|
|
79
|
+
- Automatic Pagination ([docs](https://fastapi-crudrouter.awtkns.com/pagination/))
|
|
80
|
+
- Ability to Provide Custom Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
|
|
81
|
+
- Dynamic Generation of Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
|
|
82
|
+
- Ability to Add, Customize, or Disable Specific Routes ([docs](https://fastapi-crudrouter.awtkns.com/routing/))
|
|
83
|
+
- Native Support for FastAPI Dependency Injection ([docs](https://fastapi-crudrouter.awtkns.com/dependencies/))
|
|
84
|
+
|
|
85
|
+
## Supported Backends / ORMs
|
|
86
|
+
fastapi-crudrouter currently supports a number of backends / ORMs. Listed below are the backends currently supported. This list will
|
|
87
|
+
likely grow in future releases.
|
|
88
|
+
|
|
89
|
+
- In Memory ([docs](https://fastapi-crudrouter.awtkns.com/backends/memory/))
|
|
90
|
+
- SQLAlchemy ([docs](https://fastapi-crudrouter.awtkns.com/backends/sqlalchemy/))
|
|
91
|
+
- Databases (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/async/))
|
|
92
|
+
- Gino (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/gino.html))
|
|
93
|
+
- Ormar (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/ormar/))
|
|
94
|
+
- Tortoise ORM (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/tortoise/))
|
|
95
|
+
|
|
96
|
+
## OpenAPI Support
|
|
97
|
+
By default, all routes generated by the CRUDRouter will be documented according to OpenAPI spec.
|
|
98
|
+
|
|
99
|
+
Below are the default routes created by the CRUDRouter shown in the generated OpenAPI documentation.
|
|
100
|
+
|
|
101
|
+

|
|
102
|
+
|
|
103
|
+
The CRUDRouter is able to dynamically generate detailed documentation based on the models given to it.
|
|
104
|
+
|
|
105
|
+

|
|
106
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/awtkns/fastapi-crudrouter/master/docs/en/docs/assets/logo.png" height="200" />
|
|
3
|
+
</p>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<em>⚡ Create CRUD routes with lighting speed</em> ⚡</br>
|
|
6
|
+
<sub>A dynamic FastAPI router that automatically creates CRUD routes for your models</sub>
|
|
7
|
+
</p>
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/awtkns/fastapi-crudrouter/.github/workflows/pytest.yml?color=%2334D058" />
|
|
10
|
+
<img alt="Downloads" src="https://img.shields.io/pypi/dm/fastapi-crudrouter?color=%2334D058" />
|
|
11
|
+
<a href="https://pypi.org/project/fastapi-crudrouter" target="_blank">
|
|
12
|
+
<img src="https://img.shields.io/pypi/v/fastapi-crudrouter?color=%2334D058&label=pypi%20package" alt="Package version">
|
|
13
|
+
</a>
|
|
14
|
+
<img alt="License" src="https://img.shields.io/github/license/awtkns/fastapi-crudrouter?color=%2334D058" />
|
|
15
|
+
</p>
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/fastapi-crudrouter">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
**Documentation**: <a href="https://fastapi-crudrouter.awtkns.com" target="_blank">https://fastapi-crudrouter.awtkns.com</a>
|
|
23
|
+
|
|
24
|
+
**Source Code**: <a href="https://github.com/awtkns/fastapi-crudrouter" target="_blank">https://github.com/awtkns/fastapi-crudrouter</a>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
Tired of rewriting generic CRUD routes? Need to rapidly prototype a feature for a presentation
|
|
28
|
+
or a hackathon? Thankfully, [fastapi-crudrouter](https://github.com/awtkns/fastapi-crudrouter) has your back. As an
|
|
29
|
+
extension to the APIRouter included with [FastAPI](https://fastapi.tiangolo.com/), the FastAPI CRUDRouter will automatically
|
|
30
|
+
generate and document your CRUD routes for you, all you have to do is pass your model and maybe your database connection.
|
|
31
|
+
|
|
32
|
+
FastAPI-CRUDRouter is **lighting fast**, well tested, and **production ready**.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
```bash
|
|
37
|
+
pip install fastapi-crudrouter
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Basic Usage
|
|
41
|
+
Below is a simple example of what the CRUDRouter can do. In just ten lines of code, you can generate all
|
|
42
|
+
the crud routes you need for any model. A full list of the routes generated can be found [here](https://fastapi-crudrouter.awtkns.com/routing).
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pydantic import BaseModel
|
|
46
|
+
from fastapi import FastAPI
|
|
47
|
+
from fastapi_crudrouter import MemoryCRUDRouter as CRUDRouter
|
|
48
|
+
|
|
49
|
+
class Potato(BaseModel):
|
|
50
|
+
id: int
|
|
51
|
+
color: str
|
|
52
|
+
mass: float
|
|
53
|
+
|
|
54
|
+
app = FastAPI()
|
|
55
|
+
app.include_router(CRUDRouter(schema=Potato))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Advanced Usage
|
|
59
|
+
fastapi-crudrouter provides a number of features that allow you to get the most out of your automatically generated CRUD
|
|
60
|
+
routes. Listed below are some highlights.
|
|
61
|
+
|
|
62
|
+
- Automatic Pagination ([docs](https://fastapi-crudrouter.awtkns.com/pagination/))
|
|
63
|
+
- Ability to Provide Custom Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
|
|
64
|
+
- Dynamic Generation of Create and Update Schemas ([docs](https://fastapi-crudrouter.awtkns.com/schemas/))
|
|
65
|
+
- Ability to Add, Customize, or Disable Specific Routes ([docs](https://fastapi-crudrouter.awtkns.com/routing/))
|
|
66
|
+
- Native Support for FastAPI Dependency Injection ([docs](https://fastapi-crudrouter.awtkns.com/dependencies/))
|
|
67
|
+
|
|
68
|
+
## Supported Backends / ORMs
|
|
69
|
+
fastapi-crudrouter currently supports a number of backends / ORMs. Listed below are the backends currently supported. This list will
|
|
70
|
+
likely grow in future releases.
|
|
71
|
+
|
|
72
|
+
- In Memory ([docs](https://fastapi-crudrouter.awtkns.com/backends/memory/))
|
|
73
|
+
- SQLAlchemy ([docs](https://fastapi-crudrouter.awtkns.com/backends/sqlalchemy/))
|
|
74
|
+
- Databases (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/async/))
|
|
75
|
+
- Gino (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/gino.html))
|
|
76
|
+
- Ormar (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/ormar/))
|
|
77
|
+
- Tortoise ORM (async) ([docs](https://fastapi-crudrouter.awtkns.com/backends/tortoise/))
|
|
78
|
+
|
|
79
|
+
## OpenAPI Support
|
|
80
|
+
By default, all routes generated by the CRUDRouter will be documented according to OpenAPI spec.
|
|
81
|
+
|
|
82
|
+
Below are the default routes created by the CRUDRouter shown in the generated OpenAPI documentation.
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
The CRUDRouter is able to dynamically generate detailed documentation based on the models given to it.
|
|
87
|
+
|
|
88
|
+

|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Callable, Generic, List, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
from fastapi.types import DecoratedCallable
|
|
7
|
+
|
|
8
|
+
from ._types import DEPENDENCIES, T
|
|
9
|
+
from ._utils import pagination_factory, schema_factory
|
|
10
|
+
|
|
11
|
+
NOT_FOUND = HTTPException(404, "Item not found")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CRUDGenerator(Generic[T], APIRouter, ABC):
|
|
15
|
+
schema: Type[T]
|
|
16
|
+
create_schema: Type[T]
|
|
17
|
+
update_schema: Type[T]
|
|
18
|
+
_base_path: str = "/"
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
schema: Type[T],
|
|
23
|
+
create_schema: Optional[Type[T]] = None,
|
|
24
|
+
update_schema: Optional[Type[T]] = None,
|
|
25
|
+
prefix: Optional[str] = None,
|
|
26
|
+
tags: Optional[List[str]] = None,
|
|
27
|
+
paginate: Optional[int] = None,
|
|
28
|
+
get_all_route: Union[bool, DEPENDENCIES] = True,
|
|
29
|
+
get_one_route: Union[bool, DEPENDENCIES] = True,
|
|
30
|
+
create_route: Union[bool, DEPENDENCIES] = True,
|
|
31
|
+
update_route: Union[bool, DEPENDENCIES] = True,
|
|
32
|
+
delete_one_route: Union[bool, DEPENDENCIES] = True,
|
|
33
|
+
delete_all_route: Union[bool, DEPENDENCIES] = True,
|
|
34
|
+
trailing_slash: bool = True,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.schema = schema
|
|
38
|
+
self.pagination = pagination_factory(max_limit=paginate)
|
|
39
|
+
self._pk: str = self._pk if hasattr(self, "_pk") else "id"
|
|
40
|
+
self.create_schema = (
|
|
41
|
+
create_schema
|
|
42
|
+
if create_schema
|
|
43
|
+
else schema_factory(self.schema, pk_field_name=self._pk, name="Create")
|
|
44
|
+
)
|
|
45
|
+
self.update_schema = (
|
|
46
|
+
update_schema
|
|
47
|
+
if update_schema
|
|
48
|
+
else schema_factory(self.schema, pk_field_name=self._pk, name="Update")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
prefix = str(prefix if prefix else self.schema.__name__).lower()
|
|
52
|
+
prefix = self._base_path + prefix.strip("/")
|
|
53
|
+
tags = tags or [prefix.strip("/").capitalize()]
|
|
54
|
+
path = "/" if trailing_slash else ""
|
|
55
|
+
path_param = "/{item_id}" + path
|
|
56
|
+
|
|
57
|
+
super().__init__(prefix=prefix, tags=tags, **kwargs)
|
|
58
|
+
|
|
59
|
+
if get_all_route:
|
|
60
|
+
self._add_api_route(
|
|
61
|
+
path,
|
|
62
|
+
self._get_all(),
|
|
63
|
+
methods=["GET"],
|
|
64
|
+
response_model=Optional[List[self.schema]], # type: ignore
|
|
65
|
+
summary="Get All",
|
|
66
|
+
dependencies=get_all_route,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if create_route:
|
|
70
|
+
self._add_api_route(
|
|
71
|
+
path,
|
|
72
|
+
self._create(),
|
|
73
|
+
methods=["POST"],
|
|
74
|
+
response_model=self.schema,
|
|
75
|
+
summary="Create One",
|
|
76
|
+
dependencies=create_route,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if delete_all_route:
|
|
80
|
+
self._add_api_route(
|
|
81
|
+
path,
|
|
82
|
+
self._delete_all(),
|
|
83
|
+
methods=["DELETE"],
|
|
84
|
+
response_model=Optional[List[self.schema]], # type: ignore
|
|
85
|
+
summary="Delete All",
|
|
86
|
+
dependencies=delete_all_route,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if get_one_route:
|
|
90
|
+
self._add_api_route(
|
|
91
|
+
path_param,
|
|
92
|
+
self._get_one(),
|
|
93
|
+
methods=["GET"],
|
|
94
|
+
response_model=self.schema,
|
|
95
|
+
summary="Get One",
|
|
96
|
+
dependencies=get_one_route,
|
|
97
|
+
error_responses=[NOT_FOUND],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if update_route:
|
|
101
|
+
self._add_api_route(
|
|
102
|
+
path_param,
|
|
103
|
+
self._update(),
|
|
104
|
+
methods=["PUT"],
|
|
105
|
+
response_model=self.schema,
|
|
106
|
+
summary="Update One",
|
|
107
|
+
dependencies=update_route,
|
|
108
|
+
error_responses=[NOT_FOUND],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if delete_one_route:
|
|
112
|
+
self._add_api_route(
|
|
113
|
+
path_param,
|
|
114
|
+
self._delete_one(),
|
|
115
|
+
methods=["DELETE"],
|
|
116
|
+
response_model=self.schema,
|
|
117
|
+
summary="Delete One",
|
|
118
|
+
dependencies=delete_one_route,
|
|
119
|
+
error_responses=[NOT_FOUND],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _add_api_route(
|
|
123
|
+
self,
|
|
124
|
+
path: str,
|
|
125
|
+
endpoint: Callable[..., Any],
|
|
126
|
+
dependencies: Union[bool, DEPENDENCIES],
|
|
127
|
+
error_responses: Optional[List[HTTPException]] = None,
|
|
128
|
+
**kwargs: Any,
|
|
129
|
+
) -> None:
|
|
130
|
+
dependencies = [] if isinstance(dependencies, bool) else dependencies
|
|
131
|
+
responses: Any = (
|
|
132
|
+
{err.status_code: {"detail": err.detail} for err in error_responses}
|
|
133
|
+
if error_responses
|
|
134
|
+
else None
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
super().add_api_route(
|
|
138
|
+
path, endpoint, dependencies=dependencies, responses=responses, **kwargs
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def api_route(
|
|
142
|
+
self, path: str, *args: Any, **kwargs: Any
|
|
143
|
+
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
|
144
|
+
"""Overrides and exiting route if it exists"""
|
|
145
|
+
methods = kwargs["methods"] if "methods" in kwargs else ["GET"]
|
|
146
|
+
self.remove_api_route(path, methods)
|
|
147
|
+
return super().api_route(path, *args, **kwargs)
|
|
148
|
+
|
|
149
|
+
def get(
|
|
150
|
+
self, path: str, *args: Any, **kwargs: Any
|
|
151
|
+
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
|
152
|
+
self.remove_api_route(path, ["Get"])
|
|
153
|
+
return super().get(path, *args, **kwargs)
|
|
154
|
+
|
|
155
|
+
def post(
|
|
156
|
+
self, path: str, *args: Any, **kwargs: Any
|
|
157
|
+
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
|
158
|
+
self.remove_api_route(path, ["POST"])
|
|
159
|
+
return super().post(path, *args, **kwargs)
|
|
160
|
+
|
|
161
|
+
def put(
|
|
162
|
+
self, path: str, *args: Any, **kwargs: Any
|
|
163
|
+
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
|
164
|
+
self.remove_api_route(path, ["PUT"])
|
|
165
|
+
return super().put(path, *args, **kwargs)
|
|
166
|
+
|
|
167
|
+
def delete(
|
|
168
|
+
self, path: str, *args: Any, **kwargs: Any
|
|
169
|
+
) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
|
170
|
+
self.remove_api_route(path, ["DELETE"])
|
|
171
|
+
return super().delete(path, *args, **kwargs)
|
|
172
|
+
|
|
173
|
+
def remove_api_route(self, path: str, methods: List[str]) -> None:
|
|
174
|
+
methods_ = set(methods)
|
|
175
|
+
|
|
176
|
+
for route in self.routes:
|
|
177
|
+
if (
|
|
178
|
+
route.path == f"{self.prefix}{path}" # type: ignore
|
|
179
|
+
and route.methods == methods_ # type: ignore
|
|
180
|
+
):
|
|
181
|
+
self.routes.remove(route)
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
|
185
|
+
raise NotImplementedError
|
|
186
|
+
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
|
189
|
+
raise NotImplementedError
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
|
193
|
+
raise NotImplementedError
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
|
197
|
+
raise NotImplementedError
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
|
201
|
+
raise NotImplementedError
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def _delete_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
|
205
|
+
raise NotImplementedError
|
|
206
|
+
|
|
207
|
+
def _raise(self, e: Exception, status_code: int = 422) -> HTTPException:
|
|
208
|
+
raise HTTPException(422, ", ".join(e.args)) from e
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def get_routes() -> List[str]:
|
|
212
|
+
return ["get_all", "create", "delete_all", "get_one", "update", "delete_one"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Coroutine, List, Optional, Type, Union, cast
|
|
4
|
+
|
|
5
|
+
from tortoise.models import Model
|
|
6
|
+
|
|
7
|
+
from . import NOT_FOUND, CRUDGenerator
|
|
8
|
+
from ._types import DEPENDENCIES, PAGINATION
|
|
9
|
+
from ._types import PYDANTIC_SCHEMA as SCHEMA
|
|
10
|
+
|
|
11
|
+
tortoise_installed = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CALLABLE = Callable[..., Coroutine[Any, Any, Model]]
|
|
15
|
+
CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TortoiseCRUDRouter(CRUDGenerator[SCHEMA]):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
schema: Type[SCHEMA],
|
|
22
|
+
db_model: Type[Model],
|
|
23
|
+
create_schema: Optional[Type[SCHEMA]] = None,
|
|
24
|
+
update_schema: Optional[Type[SCHEMA]] = None,
|
|
25
|
+
prefix: Optional[str] = None,
|
|
26
|
+
tags: Optional[List[str]] = None,
|
|
27
|
+
paginate: Optional[int] = None,
|
|
28
|
+
get_all_route: Union[bool, DEPENDENCIES] = True,
|
|
29
|
+
get_one_route: Union[bool, DEPENDENCIES] = True,
|
|
30
|
+
create_route: Union[bool, DEPENDENCIES] = True,
|
|
31
|
+
update_route: Union[bool, DEPENDENCIES] = True,
|
|
32
|
+
delete_one_route: Union[bool, DEPENDENCIES] = True,
|
|
33
|
+
delete_all_route: Union[bool, DEPENDENCIES] = True,
|
|
34
|
+
**kwargs: Any
|
|
35
|
+
) -> None:
|
|
36
|
+
assert (
|
|
37
|
+
tortoise_installed
|
|
38
|
+
), "Tortoise ORM must be installed to use the TortoiseCRUDRouter."
|
|
39
|
+
|
|
40
|
+
self.db_model = db_model
|
|
41
|
+
self._pk: str = db_model.describe()["pk_field"]["db_column"]
|
|
42
|
+
|
|
43
|
+
super().__init__(
|
|
44
|
+
schema=schema,
|
|
45
|
+
create_schema=create_schema,
|
|
46
|
+
update_schema=update_schema,
|
|
47
|
+
prefix=prefix or db_model.describe()["name"].replace("None.", ""),
|
|
48
|
+
tags=tags,
|
|
49
|
+
paginate=paginate,
|
|
50
|
+
get_all_route=get_all_route,
|
|
51
|
+
get_one_route=get_one_route,
|
|
52
|
+
create_route=create_route,
|
|
53
|
+
update_route=update_route,
|
|
54
|
+
delete_one_route=delete_one_route,
|
|
55
|
+
delete_all_route=delete_all_route,
|
|
56
|
+
**kwargs
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
|
|
60
|
+
async def get_all(pagination: PAGINATION = self.pagination) -> List[Model]:
|
|
61
|
+
skip, limit = pagination.get("skip"), pagination.get("limit")
|
|
62
|
+
query = self.db_model.all().offset(cast(int, skip))
|
|
63
|
+
if limit:
|
|
64
|
+
query = query.limit(limit)
|
|
65
|
+
return await query
|
|
66
|
+
|
|
67
|
+
return get_all
|
|
68
|
+
|
|
69
|
+
def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
|
|
70
|
+
async def get_one(item_id: int) -> Model:
|
|
71
|
+
model = await self.db_model.filter(id=item_id).first()
|
|
72
|
+
|
|
73
|
+
if model:
|
|
74
|
+
return model
|
|
75
|
+
else:
|
|
76
|
+
raise NOT_FOUND
|
|
77
|
+
|
|
78
|
+
return get_one
|
|
79
|
+
|
|
80
|
+
def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
|
|
81
|
+
async def create(model: self.create_schema) -> Model: # type: ignore
|
|
82
|
+
db_model = self.db_model(**model.dict())
|
|
83
|
+
await db_model.save()
|
|
84
|
+
|
|
85
|
+
return db_model
|
|
86
|
+
|
|
87
|
+
return create
|
|
88
|
+
|
|
89
|
+
def _update(self, *args: Any, **kwargs: Any) -> CALLABLE:
|
|
90
|
+
async def update(
|
|
91
|
+
item_id: int, model: self.update_schema # type: ignore
|
|
92
|
+
) -> Model:
|
|
93
|
+
await self.db_model.filter(id=item_id).update(
|
|
94
|
+
**model.dict(exclude_unset=True)
|
|
95
|
+
)
|
|
96
|
+
return await self._get_one()(item_id)
|
|
97
|
+
|
|
98
|
+
return update
|
|
99
|
+
|
|
100
|
+
def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST:
|
|
101
|
+
async def delete_all() -> List[Model]:
|
|
102
|
+
await self.db_model.all().delete()
|
|
103
|
+
return await self._get_all()(pagination={"skip": 0, "limit": None})
|
|
104
|
+
|
|
105
|
+
return delete_all
|
|
106
|
+
|
|
107
|
+
def _delete_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
|
|
108
|
+
async def delete_one(item_id: int) -> Model:
|
|
109
|
+
model: Model = await self._get_one()(item_id)
|
|
110
|
+
await self.db_model.filter(id=item_id).delete()
|
|
111
|
+
|
|
112
|
+
return model
|
|
113
|
+
|
|
114
|
+
return delete_one
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional, Sequence, TypeVar
|
|
4
|
+
|
|
5
|
+
from fastapi.params import Depends
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
PAGINATION = Dict[str, Optional[int]]
|
|
9
|
+
PYDANTIC_SCHEMA = BaseModel
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=BaseModel)
|
|
12
|
+
DEPENDENCIES = Optional[Sequence[Depends]]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Type
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, HTTPException
|
|
6
|
+
from pydantic import create_model
|
|
7
|
+
|
|
8
|
+
from ._types import PAGINATION, T
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def schema_factory(
|
|
12
|
+
schema_cls: Type[T], pk_field_name: str = "id", name: str = "Create"
|
|
13
|
+
) -> Type[T]:
|
|
14
|
+
"""
|
|
15
|
+
Is used to create a CreateSchema which does not contain pk
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
fields = {}
|
|
19
|
+
|
|
20
|
+
for field_name, field_info in schema_cls.model_fields.items():
|
|
21
|
+
if field_name != pk_field_name:
|
|
22
|
+
|
|
23
|
+
annotation = field_info.annotation
|
|
24
|
+
|
|
25
|
+
if field_info.is_required():
|
|
26
|
+
fields[field_name] = (annotation, ...)
|
|
27
|
+
else:
|
|
28
|
+
|
|
29
|
+
default = field_info.default if field_info.default is not ... else ...
|
|
30
|
+
fields[field_name] = (annotation, default)
|
|
31
|
+
|
|
32
|
+
name = schema_cls.__name__ + name
|
|
33
|
+
|
|
34
|
+
schema: Type[T] = create_model(name, **fields)
|
|
35
|
+
return schema
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_query_validation_exception(field: str, msg: str) -> HTTPException:
|
|
39
|
+
return HTTPException(
|
|
40
|
+
422,
|
|
41
|
+
detail={
|
|
42
|
+
"detail": [
|
|
43
|
+
{"loc": ["query", field], "msg": msg, "type": "type_error.integer"}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def pagination_factory(max_limit: Optional[int] = None) -> Any:
|
|
50
|
+
"""
|
|
51
|
+
Created the pagination dependency to be used in the router
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def pagination(skip: int = 0, limit: Optional[int] = max_limit) -> PAGINATION:
|
|
55
|
+
if skip < 0:
|
|
56
|
+
raise create_query_validation_exception(
|
|
57
|
+
field="skip",
|
|
58
|
+
msg="skip query parameter must be greater or equal to zero",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if limit is not None:
|
|
62
|
+
if limit <= 0:
|
|
63
|
+
raise create_query_validation_exception(
|
|
64
|
+
field="limit", msg="limit query parameter must be greater than zero"
|
|
65
|
+
)
|
|
66
|
+
elif max_limit and max_limit < limit:
|
|
67
|
+
raise create_query_validation_exception(
|
|
68
|
+
field="limit",
|
|
69
|
+
msg=f"limit query parameter must be less than {max_limit}",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return {"skip": skip, "limit": limit}
|
|
73
|
+
|
|
74
|
+
return Depends(pagination)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi-crudrouter-v2"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
description = "A dynamic FastAPI router that automatically creates CRUD routes for your models"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "pasha_danilevich",email = "danilevitch.pasha@yandex.ru"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = ">=3.9, <4.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
17
|
+
build-backend = "poetry.core.masonry.api"
|