muffin-rest 11.0.1__tar.gz → 12.0.1__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.
- muffin_rest-12.0.1/PKG-INFO +176 -0
- muffin_rest-12.0.1/README.md +138 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/api.py +8 -12
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/errors.py +3 -2
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/filters.py +4 -4
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/handler.py +31 -34
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/marshmallow.py +2 -2
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/__init__.py +13 -10
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/utils.py +7 -7
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/openapi.py +3 -4
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/options.py +1 -1
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/filters.py +8 -5
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/handler.py +11 -11
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/openapi.py +1 -1
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/schemas.py +0 -2
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/sorting.py +4 -4
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/utils.py +2 -2
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sorting.py +4 -3
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/__init__.py +14 -15
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/filters.py +4 -4
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/sorting.py +11 -6
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/types.py +0 -2
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/pyproject.toml +7 -7
- muffin_rest-11.0.1/PKG-INFO +0 -182
- muffin_rest-11.0.1/README.rst +0 -143
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/LICENSE +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/__init__.py +9 -9
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/limits.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/filters.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/schema.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/sorting.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/types.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/__init__.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/options.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/types.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/py.typed +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/redoc.html +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/schemas.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/types.py +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/swagger.html +0 -0
- {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/utils.py +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: muffin-rest
|
|
3
|
+
Version: 12.0.1
|
|
4
|
+
Summary: The package provides enhanced support for writing REST APIs with Muffin framework
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: rest,api,muffin,asgi,asyncio,trio
|
|
7
|
+
Author: Kirill Klenov
|
|
8
|
+
Author-email: horneds@gmail.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: Trio
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Provides-Extra: peewee
|
|
23
|
+
Provides-Extra: sqlalchemy
|
|
24
|
+
Provides-Extra: yaml
|
|
25
|
+
Requires-Dist: apispec (>=6,<7)
|
|
26
|
+
Requires-Dist: marshmallow (>=3,<4)
|
|
27
|
+
Requires-Dist: marshmallow-peewee ; extra == "peewee"
|
|
28
|
+
Requires-Dist: marshmallow-sqlalchemy ; extra == "sqlalchemy"
|
|
29
|
+
Requires-Dist: muffin
|
|
30
|
+
Requires-Dist: muffin-databases ; extra == "sqlalchemy"
|
|
31
|
+
Requires-Dist: muffin-peewee-aio ; extra == "peewee"
|
|
32
|
+
Requires-Dist: pyyaml ; extra == "yaml"
|
|
33
|
+
Requires-Dist: sqlalchemy ; extra == "sqlalchemy"
|
|
34
|
+
Project-URL: Homepage, https://github.com/klen/muffin-rest
|
|
35
|
+
Project-URL: Repository, https://github.com/klen/muffin-rest
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Muffin‑REST
|
|
39
|
+
|
|
40
|
+
**Muffin‑REST** simplifies building RESTful APIs with [Muffin](https://github.com/klen/muffin) by offering:
|
|
41
|
+
|
|
42
|
+
- Declarative `API` class with resource registration
|
|
43
|
+
- Built-in filtering, sorting, pagination, and search
|
|
44
|
+
- Support for:
|
|
45
|
+
- [Peewee ORM](http://docs.peewee-orm.com/en/latest/) via `PeeweeEndpoint`
|
|
46
|
+
- [SQLAlchemy Core](https://docs.sqlalchemy.org/en/14/core/) via `SAEndpoint`
|
|
47
|
+
- [MongoDB](https://www.mongodb.com/) via `MongoEndpoint`
|
|
48
|
+
- [Swagger/OpenAPI](https://swagger.io/) autodocumentation
|
|
49
|
+
- Works with asyncio, Trio, and Curio
|
|
50
|
+
|
|
51
|
+
[](https://github.com/klen/muffin-rest/actions)
|
|
52
|
+
[](https://pypi.org/project/muffin-rest/)
|
|
53
|
+
[](https://pypi.org/project/muffin-rest/)
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- Python >= 3.10
|
|
58
|
+
- Trio requires Peewee backend
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Install core package:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install muffin-rest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add optional backend support:
|
|
69
|
+
|
|
70
|
+
- SQLAlchemy Core: `pip install muffin-rest[sqlalchemy]`
|
|
71
|
+
- Peewee ORM: `pip install muffin-rest[peewee]`
|
|
72
|
+
- YAML support for Swagger: `pip install muffin-rest[yaml]`
|
|
73
|
+
|
|
74
|
+
## Quickstart (Peewee example)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from muffin import Application
|
|
78
|
+
from muffin_rest import API
|
|
79
|
+
from muffin_rest.peewee import PeeweeEndpoint
|
|
80
|
+
from models import User # your Peewee model
|
|
81
|
+
|
|
82
|
+
app = Application("myapp")
|
|
83
|
+
api = API(title="User Service", version="1.0")
|
|
84
|
+
|
|
85
|
+
@api.route
|
|
86
|
+
class UsersEndpoint(PeeweeEndpoint):
|
|
87
|
+
class Meta:
|
|
88
|
+
model = User
|
|
89
|
+
lookup_field = "id"
|
|
90
|
+
filters = ["name", "email"]
|
|
91
|
+
ordering = ["-created_at"]
|
|
92
|
+
|
|
93
|
+
api.setup(app, prefix="/api", swagger=True)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Endpoints available:
|
|
97
|
+
|
|
98
|
+
- `GET /api/users/` — list with pagination, search, filtering
|
|
99
|
+
- `POST /api/users/` — create
|
|
100
|
+
- `GET /api/users/{id}/` — retrieve
|
|
101
|
+
- `PUT /api/users/{id}/` — replace
|
|
102
|
+
- `PATCH /api/users/{id}/` — update
|
|
103
|
+
- `DELETE /api/users/{id}/` — remove
|
|
104
|
+
- Docs: `/api/docs/`, OpenAPI spec: `/api/openapi.json`
|
|
105
|
+
|
|
106
|
+
## Usage with SQLAlchemy
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from muffin_rest import API
|
|
110
|
+
from muffin_rest.sqlalchemy import SAEndpoint
|
|
111
|
+
from models import my_table, db_engine
|
|
112
|
+
|
|
113
|
+
api = API()
|
|
114
|
+
@api.route
|
|
115
|
+
class MySAEndpoint(SAEndpoint):
|
|
116
|
+
class Meta:
|
|
117
|
+
table = my_table
|
|
118
|
+
database = db_engine
|
|
119
|
+
|
|
120
|
+
api.setup(app)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Usage with MongoDB
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from muffin_rest import API
|
|
127
|
+
from muffin_rest.mongo import MongoEndpoint
|
|
128
|
+
from models import mongo_collection
|
|
129
|
+
|
|
130
|
+
api = API()
|
|
131
|
+
@api.route
|
|
132
|
+
class MyMongoEndpoint(MongoEndpoint):
|
|
133
|
+
class Meta:
|
|
134
|
+
collection = mongo_collection
|
|
135
|
+
|
|
136
|
+
api.setup(app)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Advanced Configuration
|
|
140
|
+
|
|
141
|
+
Customize Swagger and routes via constructor:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
api = API(
|
|
145
|
+
title="Service API",
|
|
146
|
+
version="2.1",
|
|
147
|
+
swagger_ui=True,
|
|
148
|
+
openapi_path="/api/openapi.json",
|
|
149
|
+
docs_path="/api/docs/"
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Contributing & Examples
|
|
154
|
+
|
|
155
|
+
- See `examples/` for live application demos
|
|
156
|
+
- Tests in `tests/` focus on filtering, pagination, status codes
|
|
157
|
+
- Check `CHANGELOG.md` for latest changes
|
|
158
|
+
|
|
159
|
+
## Bug Tracker
|
|
160
|
+
|
|
161
|
+
Report bugs or request features:
|
|
162
|
+
https://github.com/klen/muffin-rest/issues
|
|
163
|
+
|
|
164
|
+
## Contributing
|
|
165
|
+
|
|
166
|
+
Repo: https://github.com/klen/muffin-rest
|
|
167
|
+
Pull requests, example additions, docs improvements welcome!
|
|
168
|
+
|
|
169
|
+
## Contributors
|
|
170
|
+
|
|
171
|
+
- [klen](https://github.com/klen) (Kirill Klenov)
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
Licensed under the [MIT license](http://opensource.org/licenses/MIT).
|
|
176
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Muffin‑REST
|
|
2
|
+
|
|
3
|
+
**Muffin‑REST** simplifies building RESTful APIs with [Muffin](https://github.com/klen/muffin) by offering:
|
|
4
|
+
|
|
5
|
+
- Declarative `API` class with resource registration
|
|
6
|
+
- Built-in filtering, sorting, pagination, and search
|
|
7
|
+
- Support for:
|
|
8
|
+
- [Peewee ORM](http://docs.peewee-orm.com/en/latest/) via `PeeweeEndpoint`
|
|
9
|
+
- [SQLAlchemy Core](https://docs.sqlalchemy.org/en/14/core/) via `SAEndpoint`
|
|
10
|
+
- [MongoDB](https://www.mongodb.com/) via `MongoEndpoint`
|
|
11
|
+
- [Swagger/OpenAPI](https://swagger.io/) autodocumentation
|
|
12
|
+
- Works with asyncio, Trio, and Curio
|
|
13
|
+
|
|
14
|
+
[](https://github.com/klen/muffin-rest/actions)
|
|
15
|
+
[](https://pypi.org/project/muffin-rest/)
|
|
16
|
+
[](https://pypi.org/project/muffin-rest/)
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python >= 3.10
|
|
21
|
+
- Trio requires Peewee backend
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Install core package:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install muffin-rest
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add optional backend support:
|
|
32
|
+
|
|
33
|
+
- SQLAlchemy Core: `pip install muffin-rest[sqlalchemy]`
|
|
34
|
+
- Peewee ORM: `pip install muffin-rest[peewee]`
|
|
35
|
+
- YAML support for Swagger: `pip install muffin-rest[yaml]`
|
|
36
|
+
|
|
37
|
+
## Quickstart (Peewee example)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from muffin import Application
|
|
41
|
+
from muffin_rest import API
|
|
42
|
+
from muffin_rest.peewee import PeeweeEndpoint
|
|
43
|
+
from models import User # your Peewee model
|
|
44
|
+
|
|
45
|
+
app = Application("myapp")
|
|
46
|
+
api = API(title="User Service", version="1.0")
|
|
47
|
+
|
|
48
|
+
@api.route
|
|
49
|
+
class UsersEndpoint(PeeweeEndpoint):
|
|
50
|
+
class Meta:
|
|
51
|
+
model = User
|
|
52
|
+
lookup_field = "id"
|
|
53
|
+
filters = ["name", "email"]
|
|
54
|
+
ordering = ["-created_at"]
|
|
55
|
+
|
|
56
|
+
api.setup(app, prefix="/api", swagger=True)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Endpoints available:
|
|
60
|
+
|
|
61
|
+
- `GET /api/users/` — list with pagination, search, filtering
|
|
62
|
+
- `POST /api/users/` — create
|
|
63
|
+
- `GET /api/users/{id}/` — retrieve
|
|
64
|
+
- `PUT /api/users/{id}/` — replace
|
|
65
|
+
- `PATCH /api/users/{id}/` — update
|
|
66
|
+
- `DELETE /api/users/{id}/` — remove
|
|
67
|
+
- Docs: `/api/docs/`, OpenAPI spec: `/api/openapi.json`
|
|
68
|
+
|
|
69
|
+
## Usage with SQLAlchemy
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from muffin_rest import API
|
|
73
|
+
from muffin_rest.sqlalchemy import SAEndpoint
|
|
74
|
+
from models import my_table, db_engine
|
|
75
|
+
|
|
76
|
+
api = API()
|
|
77
|
+
@api.route
|
|
78
|
+
class MySAEndpoint(SAEndpoint):
|
|
79
|
+
class Meta:
|
|
80
|
+
table = my_table
|
|
81
|
+
database = db_engine
|
|
82
|
+
|
|
83
|
+
api.setup(app)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage with MongoDB
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from muffin_rest import API
|
|
90
|
+
from muffin_rest.mongo import MongoEndpoint
|
|
91
|
+
from models import mongo_collection
|
|
92
|
+
|
|
93
|
+
api = API()
|
|
94
|
+
@api.route
|
|
95
|
+
class MyMongoEndpoint(MongoEndpoint):
|
|
96
|
+
class Meta:
|
|
97
|
+
collection = mongo_collection
|
|
98
|
+
|
|
99
|
+
api.setup(app)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Advanced Configuration
|
|
103
|
+
|
|
104
|
+
Customize Swagger and routes via constructor:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
api = API(
|
|
108
|
+
title="Service API",
|
|
109
|
+
version="2.1",
|
|
110
|
+
swagger_ui=True,
|
|
111
|
+
openapi_path="/api/openapi.json",
|
|
112
|
+
docs_path="/api/docs/"
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Contributing & Examples
|
|
117
|
+
|
|
118
|
+
- See `examples/` for live application demos
|
|
119
|
+
- Tests in `tests/` focus on filtering, pagination, status codes
|
|
120
|
+
- Check `CHANGELOG.md` for latest changes
|
|
121
|
+
|
|
122
|
+
## Bug Tracker
|
|
123
|
+
|
|
124
|
+
Report bugs or request features:
|
|
125
|
+
https://github.com/klen/muffin-rest/issues
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
Repo: https://github.com/klen/muffin-rest
|
|
130
|
+
Pull requests, example additions, docs improvements welcome!
|
|
131
|
+
|
|
132
|
+
## Contributors
|
|
133
|
+
|
|
134
|
+
- [klen](https://github.com/klen) (Kirill Klenov)
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
Licensed under the [MIT license](http://opensource.org/licenses/MIT).
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import dataclasses as dc
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, overload
|
|
8
8
|
|
|
9
9
|
from http_router import Router
|
|
10
10
|
from muffin.utils import TV, to_awaitable
|
|
@@ -27,11 +27,11 @@ class API:
|
|
|
27
27
|
|
|
28
28
|
def __init__(
|
|
29
29
|
self,
|
|
30
|
-
app:
|
|
30
|
+
app: muffin.Application | None = None,
|
|
31
31
|
prefix: str = "",
|
|
32
32
|
*,
|
|
33
33
|
openapi: bool = True,
|
|
34
|
-
servers:
|
|
34
|
+
servers: list | None = None,
|
|
35
35
|
**openapi_info,
|
|
36
36
|
):
|
|
37
37
|
"""Post initialize the API if we have an application already."""
|
|
@@ -66,8 +66,8 @@ class API:
|
|
|
66
66
|
app: muffin.Application,
|
|
67
67
|
*,
|
|
68
68
|
prefix: str = "",
|
|
69
|
-
openapi:
|
|
70
|
-
servers:
|
|
69
|
+
openapi: bool | None = None,
|
|
70
|
+
servers: list | None = None,
|
|
71
71
|
**openapi_info,
|
|
72
72
|
):
|
|
73
73
|
"""Initialize the API."""
|
|
@@ -100,16 +100,12 @@ class API:
|
|
|
100
100
|
self.router.route("/openapi.json")(openapi_json)
|
|
101
101
|
|
|
102
102
|
@overload
|
|
103
|
-
def route(self, obj: str, *paths: str, **params) -> Callable[[TV], TV]:
|
|
104
|
-
...
|
|
103
|
+
def route(self, obj: str, *paths: str, **params) -> Callable[[TV], TV]: ...
|
|
105
104
|
|
|
106
105
|
@overload
|
|
107
|
-
def route(self, obj: TVHandler, *paths: str, **params) -> TVHandler:
|
|
108
|
-
...
|
|
106
|
+
def route(self, obj: TVHandler, *paths: str, **params) -> TVHandler: ...
|
|
109
107
|
|
|
110
|
-
def route(
|
|
111
|
-
self, obj: Union[str, TVHandler], *paths: str, **params
|
|
112
|
-
) -> Union[Callable[[TV], TV], TVHandler]:
|
|
108
|
+
def route(self, obj: str | TVHandler, *paths: str, **params) -> Callable[[TV], TV] | TVHandler:
|
|
113
109
|
"""Route an endpoint by the API."""
|
|
114
110
|
from .handler import RESTBase
|
|
115
111
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Helpers to raise API errors as JSON responses."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import json
|
|
5
6
|
from http import HTTPStatus
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from muffin import ResponseError
|
|
9
10
|
|
|
@@ -16,7 +17,7 @@ class APIError(ResponseError):
|
|
|
16
17
|
|
|
17
18
|
def __init__(
|
|
18
19
|
self,
|
|
19
|
-
content:
|
|
20
|
+
content: TJSON | None = None,
|
|
20
21
|
*,
|
|
21
22
|
status_code: int = HTTPStatus.BAD_REQUEST.value,
|
|
22
23
|
**json_data,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import operator
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping
|
|
7
7
|
|
|
8
8
|
import marshmallow as ma
|
|
9
9
|
from asgi_tools._compat import json_loads
|
|
@@ -59,8 +59,8 @@ class Filter(Mutate):
|
|
|
59
59
|
name: str,
|
|
60
60
|
*,
|
|
61
61
|
field: Any = None,
|
|
62
|
-
schema_field:
|
|
63
|
-
operator:
|
|
62
|
+
schema_field: ma.fields.Field | None = None,
|
|
63
|
+
operator: str | None = None,
|
|
64
64
|
**meta,
|
|
65
65
|
):
|
|
66
66
|
"""Initialize filter.
|
|
@@ -75,7 +75,7 @@ class Filter(Mutate):
|
|
|
75
75
|
self.schema_field = schema_field or self.schema_field
|
|
76
76
|
self.default_operator = operator or self.default_operator
|
|
77
77
|
|
|
78
|
-
async def apply(self, collection: Any, data:
|
|
78
|
+
async def apply(self, collection: Any, data: Mapping | None = None):
|
|
79
79
|
"""Filter given collection."""
|
|
80
80
|
if not data:
|
|
81
81
|
return None, collection
|
|
@@ -9,9 +9,7 @@ from typing import (
|
|
|
9
9
|
Iterable,
|
|
10
10
|
Literal,
|
|
11
11
|
Mapping,
|
|
12
|
-
Optional,
|
|
13
12
|
Sequence,
|
|
14
|
-
Union,
|
|
15
13
|
cast,
|
|
16
14
|
overload,
|
|
17
15
|
)
|
|
@@ -31,7 +29,7 @@ from muffin_rest.types import TSchemaRes
|
|
|
31
29
|
|
|
32
30
|
from .errors import HandlerNotBindedError
|
|
33
31
|
from .options import RESTOptions
|
|
34
|
-
from .types import TVCollection,
|
|
32
|
+
from .types import TVCollection, TVResource
|
|
35
33
|
|
|
36
34
|
|
|
37
35
|
class RESTHandlerMeta(HandlerMeta):
|
|
@@ -39,7 +37,7 @@ class RESTHandlerMeta(HandlerMeta):
|
|
|
39
37
|
|
|
40
38
|
def __new__(mcs, name, bases, params):
|
|
41
39
|
"""Prepare options for the handler."""
|
|
42
|
-
kls = cast(type[
|
|
40
|
+
kls = cast("type[RESTBase]", super().__new__(mcs, name, bases, params))
|
|
43
41
|
kls.meta = kls.meta_class(kls)
|
|
44
42
|
|
|
45
43
|
if getattr(kls.meta, kls.meta_class.base_property, None) is not None:
|
|
@@ -58,22 +56,22 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
58
56
|
|
|
59
57
|
meta: RESTOptions
|
|
60
58
|
meta_class: type[RESTOptions] = RESTOptions
|
|
61
|
-
_api:
|
|
59
|
+
_api: API | None = None
|
|
62
60
|
|
|
63
|
-
filters:
|
|
64
|
-
sorting:
|
|
61
|
+
filters: dict[str, Any] | None = None
|
|
62
|
+
sorting: dict[str, Any] | None = None
|
|
65
63
|
|
|
66
64
|
class Meta:
|
|
67
65
|
"""Tune the handler."""
|
|
68
66
|
|
|
69
67
|
# Resource filters
|
|
70
|
-
filters: Sequence[
|
|
68
|
+
filters: Sequence[str | tuple[str, str] | Filter] = ()
|
|
71
69
|
|
|
72
70
|
# Define allowed resource sorting params
|
|
73
|
-
sorting: Sequence[
|
|
71
|
+
sorting: Sequence[str | tuple[str, dict] | Sort] = ()
|
|
74
72
|
|
|
75
73
|
# Serialize/Deserialize Schema class
|
|
76
|
-
Schema:
|
|
74
|
+
Schema: type[ma.Schema] | None = None
|
|
77
75
|
|
|
78
76
|
@classmethod
|
|
79
77
|
def __route__(cls, router, *paths, **params):
|
|
@@ -84,9 +82,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
84
82
|
|
|
85
83
|
else:
|
|
86
84
|
router.bind(cls, f"/{ cls.meta.name }", methods=methods, **params)
|
|
87
|
-
router.bind(
|
|
88
|
-
cls, f"/{ cls.meta.name }/{{{ cls.meta.name_id }}}", methods=methods, **params
|
|
89
|
-
)
|
|
85
|
+
router.bind(cls, f"/{ cls.meta.name }/{{pk}}", methods=methods, **params)
|
|
90
86
|
|
|
91
87
|
for _, method in inspect.getmembers(cls, lambda m: hasattr(m, "__route__")):
|
|
92
88
|
paths, methods = method.__route__
|
|
@@ -94,7 +90,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
94
90
|
|
|
95
91
|
return cls
|
|
96
92
|
|
|
97
|
-
async def __call__(self, request: Request, *, method_name:
|
|
93
|
+
async def __call__(self, request: Request, *, method_name: str | None = None, **_) -> Any:
|
|
98
94
|
"""Dispatch the given request by HTTP method."""
|
|
99
95
|
self.auth = await self.authorize(request)
|
|
100
96
|
|
|
@@ -151,7 +147,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
151
147
|
|
|
152
148
|
async def prepare_resource(self, request: Request) -> Any:
|
|
153
149
|
"""Load a resource."""
|
|
154
|
-
return request["path_params"].get(
|
|
150
|
+
return request["path_params"].get("pk")
|
|
155
151
|
|
|
156
152
|
async def filter(self, request: Request, collection: TVCollection) -> tuple[TVCollection, Any]:
|
|
157
153
|
"""Filter the collection."""
|
|
@@ -190,7 +186,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
190
186
|
@abc.abstractmethod
|
|
191
187
|
async def paginate(
|
|
192
188
|
self, request: Request, *, limit: int = 0, offset: int = 0
|
|
193
|
-
) -> tuple[Any,
|
|
189
|
+
) -> tuple[Any, int | None]:
|
|
194
190
|
"""Paginate the results."""
|
|
195
191
|
raise NotImplementedError
|
|
196
192
|
|
|
@@ -215,7 +211,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
215
211
|
# Parse data
|
|
216
212
|
# -----------
|
|
217
213
|
def get_schema(
|
|
218
|
-
self, request: Request, *, resource:
|
|
214
|
+
self, request: Request, *, resource: TVResource | None = None, **schema_options
|
|
219
215
|
) -> ma.Schema:
|
|
220
216
|
"""Initialize marshmallow schema for serialization/deserialization."""
|
|
221
217
|
query = request.url.query
|
|
@@ -232,34 +228,37 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
232
228
|
|
|
233
229
|
return data
|
|
234
230
|
|
|
235
|
-
async def load(
|
|
236
|
-
self, request: Request, resource: Optional[TVResource] = None, **schema_options
|
|
237
|
-
) -> TVData[TVResource]:
|
|
231
|
+
async def load(self, request: Request, resource: TVResource | None = None, **schema_options):
|
|
238
232
|
"""Load data from request and create/update a resource."""
|
|
239
233
|
schema = self.get_schema(request, resource=resource, **schema_options)
|
|
240
|
-
data = cast(
|
|
241
|
-
return cast(
|
|
234
|
+
data = cast("Mapping | list", await self.load_data(request))
|
|
235
|
+
return cast(
|
|
236
|
+
"TVResource | list[TVResource]",
|
|
237
|
+
await load_data(data, schema, partial=resource is not None),
|
|
238
|
+
)
|
|
242
239
|
|
|
243
240
|
@overload
|
|
244
241
|
async def dump( # type: ignore[misc]
|
|
245
|
-
self, request, data:
|
|
242
|
+
self, request, data: TVResource | Iterable[TVResource], *, many: Literal[True]
|
|
246
243
|
) -> list[TSchemaRes]: ...
|
|
247
244
|
|
|
248
245
|
@overload
|
|
249
|
-
async def dump(
|
|
246
|
+
async def dump(
|
|
247
|
+
self, request, data: TVResource | Iterable[TVResource], *, many: bool = False
|
|
248
|
+
) -> TSchemaRes: ...
|
|
250
249
|
|
|
251
250
|
async def dump(
|
|
252
251
|
self,
|
|
253
252
|
request: Request,
|
|
254
|
-
data:
|
|
253
|
+
data: TVResource | Iterable[TVResource],
|
|
255
254
|
*,
|
|
256
255
|
many: bool = False,
|
|
257
|
-
) ->
|
|
256
|
+
) -> TSchemaRes | list[TSchemaRes]:
|
|
258
257
|
"""Serialize the given response."""
|
|
259
258
|
schema = self.get_schema(request)
|
|
260
259
|
return schema.dump(data, many=many)
|
|
261
260
|
|
|
262
|
-
async def get(self, request: Request, *, resource:
|
|
261
|
+
async def get(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
|
|
263
262
|
"""Get a resource or a collection of resources.
|
|
264
263
|
|
|
265
264
|
Specify a path param to load a resource.
|
|
@@ -269,11 +268,9 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
269
268
|
if resource
|
|
270
269
|
else self.dump(request, data=self.collection, many=True)
|
|
271
270
|
)
|
|
272
|
-
return ResponseJSON(res)
|
|
271
|
+
return ResponseJSON(res) # type: ignore[type-var]
|
|
273
272
|
|
|
274
|
-
async def post(
|
|
275
|
-
self, request: Request, *, resource: Optional[TVResource] = None
|
|
276
|
-
) -> ResponseJSON:
|
|
273
|
+
async def post(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
|
|
277
274
|
"""Create a resource.
|
|
278
275
|
|
|
279
276
|
The method accepts a single resource's data or a list of resources to create.
|
|
@@ -283,19 +280,19 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
283
280
|
if many:
|
|
284
281
|
data = await self.save_many(request, data, update=resource is not None)
|
|
285
282
|
else:
|
|
286
|
-
data = await self.save(request, cast(TVResource, data), update=resource is not None)
|
|
283
|
+
data = await self.save(request, cast("TVResource", data), update=resource is not None)
|
|
287
284
|
|
|
288
285
|
res = await self.dump(request, data, many=many)
|
|
289
286
|
return ResponseJSON(res)
|
|
290
287
|
|
|
291
|
-
async def put(self, request: Request, *, resource:
|
|
288
|
+
async def put(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
|
|
292
289
|
"""Update a resource."""
|
|
293
290
|
if resource is None:
|
|
294
291
|
raise APIError.NOT_FOUND()
|
|
295
292
|
|
|
296
293
|
return await self.post(request, resource=resource)
|
|
297
294
|
|
|
298
|
-
async def delete(self, request: Request, resource:
|
|
295
|
+
async def delete(self, request: Request, resource: TVResource | None = None):
|
|
299
296
|
"""Delete a resource."""
|
|
300
297
|
if resource is None:
|
|
301
298
|
raise APIError.NOT_FOUND()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from marshmallow import Schema, ValidationError
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from collections.abc import Mapping
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
async def load_data(data:
|
|
13
|
+
async def load_data(data: Mapping | list, schema: Schema | None = None, **params):
|
|
14
14
|
if schema is None:
|
|
15
15
|
return data
|
|
16
16
|
|