muffin-rest 9.4.0__py3-none-any.whl → 10.1.0__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.
- muffin_rest/handler.py +15 -14
- muffin_rest/limits.py +15 -11
- muffin_rest/marshmallow.py +4 -10
- muffin_rest/options.py +1 -16
- muffin_rest/peewee/utils.py +2 -1
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.1.0.dist-info}/METADATA +3 -3
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.1.0.dist-info}/RECORD +9 -9
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.1.0.dist-info}/WHEEL +1 -1
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.1.0.dist-info}/LICENSE +0 -0
muffin_rest/handler.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Base class for API REST Handlers."""
|
|
2
|
+
|
|
2
3
|
import abc
|
|
3
4
|
import inspect
|
|
4
5
|
from typing import (
|
|
@@ -7,6 +8,7 @@ from typing import (
|
|
|
7
8
|
Generic,
|
|
8
9
|
Iterable,
|
|
9
10
|
Literal,
|
|
11
|
+
Mapping,
|
|
10
12
|
Optional,
|
|
11
13
|
Sequence,
|
|
12
14
|
Union,
|
|
@@ -97,8 +99,6 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
97
99
|
self.auth = await self.authorize(request)
|
|
98
100
|
|
|
99
101
|
meta = self.meta
|
|
100
|
-
if meta.rate_limit:
|
|
101
|
-
await self.rate_limit(request)
|
|
102
102
|
|
|
103
103
|
self.collection = await self.prepare_collection(request)
|
|
104
104
|
resource = await self.prepare_resource(request)
|
|
@@ -142,11 +142,6 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
142
142
|
raise APIError.UNAUTHORIZED()
|
|
143
143
|
return auth
|
|
144
144
|
|
|
145
|
-
async def rate_limit(self, request: Request):
|
|
146
|
-
"""Default rate limit method. Proxy rate limit to self.api."""
|
|
147
|
-
if not await self.meta.rate_limiter.check(f"{self.auth}"):
|
|
148
|
-
raise APIError.TOO_MANY_REQUESTS()
|
|
149
|
-
|
|
150
145
|
# Prepare data
|
|
151
146
|
# ------------
|
|
152
147
|
@abc.abstractmethod
|
|
@@ -228,24 +223,30 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
228
223
|
schema_options.setdefault("exclude", query.get("schema_exclude", ()))
|
|
229
224
|
return self.meta.Schema(**schema_options)
|
|
230
225
|
|
|
226
|
+
async def load_data(self, request: Request):
|
|
227
|
+
"""Load data from request and create/update a resource."""
|
|
228
|
+
try:
|
|
229
|
+
data = await request.data(raise_errors=True)
|
|
230
|
+
except (ValueError, TypeError) as err:
|
|
231
|
+
raise APIError.BAD_REQUEST(str(err)) from err
|
|
232
|
+
|
|
233
|
+
return data
|
|
234
|
+
|
|
231
235
|
async def load(
|
|
232
236
|
self, request: Request, resource: Optional[TVResource] = None, **schema_options
|
|
233
237
|
) -> TVData[TVResource]:
|
|
234
238
|
"""Load data from request and create/update a resource."""
|
|
235
239
|
schema = self.get_schema(request, resource=resource, **schema_options)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
240
|
+
data = cast(Union[Mapping, list], await self.load_data(request))
|
|
241
|
+
return cast(TVData[TVResource], await load_data(data, schema, partial=resource is not None))
|
|
239
242
|
|
|
240
243
|
@overload
|
|
241
244
|
async def dump( # type: ignore[misc]
|
|
242
245
|
self, request, data: TVData, *, many: Literal[True]
|
|
243
|
-
) -> list[TSchemaRes]:
|
|
244
|
-
...
|
|
246
|
+
) -> list[TSchemaRes]: ...
|
|
245
247
|
|
|
246
248
|
@overload
|
|
247
|
-
async def dump(self, request, data: TVData, *, many: bool = False) -> TSchemaRes:
|
|
248
|
-
...
|
|
249
|
+
async def dump(self, request, data: TVData, *, many: bool = False) -> TSchemaRes: ...
|
|
249
250
|
|
|
250
251
|
async def dump(
|
|
251
252
|
self,
|
muffin_rest/limits.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
from time import time
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class RateLimiter(abc.ABC):
|
|
6
7
|
"""Rate limiter."""
|
|
7
8
|
|
|
8
|
-
def __init__(self, limit: int, period: int, **opts):
|
|
9
|
+
def __init__(self, limit: int, *, period: int = 60, **opts):
|
|
9
10
|
"""Initialize the rate limiter.
|
|
10
11
|
|
|
11
12
|
Args:
|
|
@@ -21,26 +22,29 @@ class RateLimiter(abc.ABC):
|
|
|
21
22
|
raise NotImplementedError
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
RATE_LIMITS = {}
|
|
25
|
-
|
|
26
|
-
|
|
27
25
|
class MemoryRateLimiter(RateLimiter):
|
|
28
26
|
"""Memory rate limiter. Do not use in production."""
|
|
29
27
|
|
|
28
|
+
def __init__(self, limit: int, **opts):
|
|
29
|
+
super().__init__(limit, **opts)
|
|
30
|
+
self.storage: dict[Any, tuple[float, int]] = {}
|
|
31
|
+
|
|
30
32
|
async def check(self, key: str) -> bool:
|
|
31
33
|
"""Check the request."""
|
|
32
34
|
now = time()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
storage = self.storage
|
|
36
|
+
|
|
37
|
+
if key not in storage:
|
|
38
|
+
storage[key] = (now, 1)
|
|
35
39
|
return True
|
|
36
40
|
|
|
37
|
-
last, count =
|
|
41
|
+
last, count = storage[key]
|
|
38
42
|
if now - last > self.period:
|
|
39
|
-
|
|
43
|
+
storage[key] = (now, 1)
|
|
40
44
|
return True
|
|
41
45
|
|
|
42
46
|
if count < self.limit:
|
|
43
|
-
|
|
47
|
+
storage[key] = (last, count + 1)
|
|
44
48
|
return True
|
|
45
49
|
|
|
46
50
|
return False
|
|
@@ -51,7 +55,7 @@ class RedisRateLimiter(RateLimiter):
|
|
|
51
55
|
|
|
52
56
|
# TODO: Asyncio lock
|
|
53
57
|
|
|
54
|
-
def __init__(self, limit: int,
|
|
58
|
+
def __init__(self, limit: int, *, redis, **opts):
|
|
55
59
|
"""Initialize the rate limiter.
|
|
56
60
|
|
|
57
61
|
Args:
|
|
@@ -59,7 +63,7 @@ class RedisRateLimiter(RateLimiter):
|
|
|
59
63
|
period (int): The period of time in seconds.
|
|
60
64
|
redis (aioredis.Redis): The Redis connection.
|
|
61
65
|
"""
|
|
62
|
-
super().__init__(limit,
|
|
66
|
+
super().__init__(limit, **opts)
|
|
63
67
|
self.redis = redis
|
|
64
68
|
|
|
65
69
|
async def check(self, key: str) -> bool:
|
muffin_rest/marshmallow.py
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from typing import TYPE_CHECKING, Optional, Union, cast
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
5
4
|
|
|
6
5
|
from marshmallow import Schema, ValidationError
|
|
7
6
|
|
|
8
7
|
from muffin_rest.errors import APIError
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
11
|
-
from
|
|
10
|
+
from collections.abc import Mapping
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
async def load_data(
|
|
15
|
-
try:
|
|
16
|
-
data = await request.data(raise_errors=True)
|
|
17
|
-
except (ValueError, TypeError) as err:
|
|
18
|
-
raise APIError.BAD_REQUEST(str(err)) from err
|
|
19
|
-
|
|
13
|
+
async def load_data(data: Union[Mapping, list], schema: Optional[Schema] = None, **params):
|
|
20
14
|
if schema is None:
|
|
21
15
|
return data
|
|
22
16
|
|
|
23
17
|
try:
|
|
24
|
-
return schema.load(
|
|
18
|
+
return schema.load(data, many=isinstance(data, list), **params)
|
|
25
19
|
except ValidationError as err:
|
|
26
20
|
raise APIError.BAD_REQUEST("Bad request data", errors=err.messages) from err
|
muffin_rest/options.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""REST Options."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import ClassVar
|
|
4
4
|
|
|
5
5
|
import marshmallow as ma
|
|
6
6
|
|
|
7
|
-
from muffin_rest.limits import MemoryRateLimiter, RateLimiter
|
|
8
|
-
|
|
9
7
|
from .filters import Filters
|
|
10
8
|
from .sorting import Sorting
|
|
11
9
|
|
|
@@ -53,14 +51,6 @@ class RESTOptions:
|
|
|
53
51
|
schema_meta: ClassVar[dict] = {}
|
|
54
52
|
schema_unknown: str = ma.EXCLUDE
|
|
55
53
|
|
|
56
|
-
# Rate Limiting
|
|
57
|
-
# -------------
|
|
58
|
-
|
|
59
|
-
rate_limit: int = 0
|
|
60
|
-
rate_limit_period: int = 60
|
|
61
|
-
rate_limit_cls: type[RateLimiter] = MemoryRateLimiter
|
|
62
|
-
rate_limit_cls_opts: ClassVar[dict[str, Any]] = {}
|
|
63
|
-
|
|
64
54
|
def __init__(self, cls):
|
|
65
55
|
"""Inherit meta options."""
|
|
66
56
|
for base in reversed(cls.mro()):
|
|
@@ -86,11 +76,6 @@ class RESTOptions:
|
|
|
86
76
|
if not self.limit_max:
|
|
87
77
|
self.limit_max = self.limit
|
|
88
78
|
|
|
89
|
-
if self.rate_limit:
|
|
90
|
-
self.rate_limiter = self.rate_limit_cls(
|
|
91
|
-
self.rate_limit, self.rate_limit_period, **self.rate_limit_cls_opts
|
|
92
|
-
)
|
|
93
|
-
|
|
94
79
|
def setup_schema_meta(self, _):
|
|
95
80
|
"""Generate meta for schemas."""
|
|
96
81
|
return type(
|
muffin_rest/peewee/utils.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Support filters for Peewee ORM."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
from typing import TYPE_CHECKING, Optional
|
|
@@ -8,7 +9,7 @@ if TYPE_CHECKING:
|
|
|
8
9
|
from peewee import Field
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def get_model_field_by_name(handler, name: str, stacklevel=5) -> Optional[Field]:
|
|
12
|
+
def get_model_field_by_name(handler, name: str, *, stacklevel=5) -> Optional[Field]:
|
|
12
13
|
"""Get model field by name."""
|
|
13
14
|
fields = handler.meta.model._meta.fields
|
|
14
15
|
candidate = fields.get(name)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: muffin-rest
|
|
3
|
-
Version:
|
|
3
|
+
Version: 10.1.0
|
|
4
4
|
Summary: The package provides enhanced support for writing REST APIs with Muffin framework
|
|
5
|
-
Home-page: https://github.com/klen/muffin-rest
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: rest,api,muffin,asgi,asyncio,trio
|
|
8
7
|
Author: Kirill Klenov
|
|
@@ -33,6 +32,7 @@ Requires-Dist: muffin-databases ; extra == "sqlalchemy"
|
|
|
33
32
|
Requires-Dist: muffin-peewee-aio ; extra == "peewee"
|
|
34
33
|
Requires-Dist: pyyaml ; extra == "yaml"
|
|
35
34
|
Requires-Dist: sqlalchemy ; extra == "sqlalchemy"
|
|
35
|
+
Project-URL: Homepage, https://github.com/klen/muffin-rest
|
|
36
36
|
Project-URL: Repository, https://github.com/klen/muffin-rest
|
|
37
37
|
Description-Content-Type: text/x-rst
|
|
38
38
|
|
|
@@ -2,9 +2,9 @@ muffin_rest/__init__.py,sha256=NBZeOEJgQHtFFhVgd9d0fpApFRgU405sbm0cu1y1MOU,1242
|
|
|
2
2
|
muffin_rest/api.py,sha256=zssoHjqTsa8UCAyxj6TxQVluYPX9Sigyiph6SRxBY6I,3870
|
|
3
3
|
muffin_rest/errors.py,sha256=mxEBhNPo3pwpG2em6zaQonbfRgHFBJ3I8WunVYWDjvM,1163
|
|
4
4
|
muffin_rest/filters.py,sha256=hc-fhBODwrgJM_CvXNtTVH6jIOkjLog8En0iKKYXAGU,5947
|
|
5
|
-
muffin_rest/handler.py,sha256=
|
|
6
|
-
muffin_rest/limits.py,sha256=
|
|
7
|
-
muffin_rest/marshmallow.py,sha256=
|
|
5
|
+
muffin_rest/handler.py,sha256=xRKUMzR2zISva0SbXS-piQ3Xq3d1zu-rZg6H2K7Oy0I,10824
|
|
6
|
+
muffin_rest/limits.py,sha256=Fnlu4Wj3B-BzpahLK-rDbd1GEd7CEQ3zxyOD0vee7GE,2007
|
|
7
|
+
muffin_rest/marshmallow.py,sha256=UeHxZLtETlrheDAlMZn7Xv_7vWPxvJQo-sj05nql9gM,574
|
|
8
8
|
muffin_rest/mongo/__init__.py,sha256=SiYSbX6ySJl43fw9aGREIs8ZsS8Qk_ieizoPOj4DjJc,4656
|
|
9
9
|
muffin_rest/mongo/filters.py,sha256=yIxIDVqMn6SoDgVhCqiTxYetw0hoaf_3jIvX2Vnizok,964
|
|
10
10
|
muffin_rest/mongo/schema.py,sha256=y4OEPQnlV_COTIIQ3cKmpqDpD2r18eAWn0rijQldWm0,1205
|
|
@@ -12,7 +12,7 @@ muffin_rest/mongo/sorting.py,sha256=iJBnaFwE7g_JMwpGpQkoqSqbQK9XULx1K3skiRRgLgY,
|
|
|
12
12
|
muffin_rest/mongo/types.py,sha256=jaODScgwwYbzHis3DY4bPzU1ahiMJMSwquH5_Thi-Gg,200
|
|
13
13
|
muffin_rest/mongo/utils.py,sha256=mNkLM-D6gqOA9YW2Qdw0DvE2N4LRmxLAiPMKH9WLttM,3958
|
|
14
14
|
muffin_rest/openapi.py,sha256=0QU7qrfBjGl0vl378SJC5boZZI2ogddl45fS9WL4Axw,8751
|
|
15
|
-
muffin_rest/options.py,sha256=
|
|
15
|
+
muffin_rest/options.py,sha256=38y7AasyAghXNffxX-3xgqLbWDQ1RxAhg77ze7c0Mu0,2232
|
|
16
16
|
muffin_rest/peewee/__init__.py,sha256=94DSj_ftT6fbPksHlBv40AH2HWaiZommUFOMN2jd9a4,129
|
|
17
17
|
muffin_rest/peewee/filters.py,sha256=p813eJqyTkAhmS3C1P8rWFWb9Tl33OtADjgLctqKnns,2475
|
|
18
18
|
muffin_rest/peewee/handler.py,sha256=Tk20ChTGkhzSF0K1-TFruPfXJyHfJE_fDKcQqGQeoAU,5297
|
|
@@ -21,7 +21,7 @@ muffin_rest/peewee/options.py,sha256=TimJtErC9e8B7BRiEkHiBZd71_bZbYr-FE2PIlQvfH0
|
|
|
21
21
|
muffin_rest/peewee/schemas.py,sha256=w6jBziUp40mOOjkz_4RCXuY0x5ZDIe9Ob25k1FnZSfc,469
|
|
22
22
|
muffin_rest/peewee/sorting.py,sha256=aTLL2zYeNfkamfbGuKkIClOsJktdZZZlzZKafccWxyQ,1977
|
|
23
23
|
muffin_rest/peewee/types.py,sha256=cgCXhpGHkImKwudA1lulZHz5oJswHH168AiW5MhZRCM,155
|
|
24
|
-
muffin_rest/peewee/utils.py,sha256=
|
|
24
|
+
muffin_rest/peewee/utils.py,sha256=wrDcEEeCMzK4GdMSBi7BX0XpBPRZ6pF_EHwDsZPgL1s,706
|
|
25
25
|
muffin_rest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
muffin_rest/redoc.html,sha256=GtuHIMvTuSi8Ro6bgI-G8VB94AljMyfjcZseqtBmGCY,559
|
|
27
27
|
muffin_rest/schemas.py,sha256=BW3dF82C6Q6STs4tZjej1x8Ii1rI3EZUJZR4mNNKmu4,875
|
|
@@ -33,7 +33,7 @@ muffin_rest/sqlalchemy/types.py,sha256=Exm-zAQCtPAwXvYcCTtPRqSa-wTEWRcH_v2YSsJkB
|
|
|
33
33
|
muffin_rest/swagger.html,sha256=2uGLu_KpkYf925KnDKHBJmV9pm6OHn5C3BWScESsUS8,1736
|
|
34
34
|
muffin_rest/types.py,sha256=m27-g6BI7qdSWGym4fWALBJa2ZpWR0_m0nlrDx7iTCo,566
|
|
35
35
|
muffin_rest/utils.py,sha256=c08E4HJ4SLYC-91GKPEbsyKTZ4sZbTN4qDqJbNg_HTE,2076
|
|
36
|
-
muffin_rest-
|
|
37
|
-
muffin_rest-
|
|
38
|
-
muffin_rest-
|
|
39
|
-
muffin_rest-
|
|
36
|
+
muffin_rest-10.1.0.dist-info/LICENSE,sha256=xHPkOZhjyKBMOwXpWn9IB_BVLjrrMxv2M9slKkHj2hM,1082
|
|
37
|
+
muffin_rest-10.1.0.dist-info/METADATA,sha256=eZeEf9-ojFMBHyyQLX1tkRurpNj1NMufQda6B1d6I7o,4147
|
|
38
|
+
muffin_rest-10.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
39
|
+
muffin_rest-10.1.0.dist-info/RECORD,,
|
|
File without changes
|