muffin-rest 9.4.0__py3-none-any.whl → 10.0.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 +3 -11
- muffin_rest/limits.py +15 -11
- muffin_rest/options.py +1 -16
- muffin_rest/peewee/utils.py +2 -1
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.0.0.dist-info}/METADATA +3 -3
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.0.0.dist-info}/RECORD +8 -8
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.0.0.dist-info}/WHEEL +1 -1
- {muffin_rest-9.4.0.dist-info → muffin_rest-10.0.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 (
|
|
@@ -97,8 +98,6 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
97
98
|
self.auth = await self.authorize(request)
|
|
98
99
|
|
|
99
100
|
meta = self.meta
|
|
100
|
-
if meta.rate_limit:
|
|
101
|
-
await self.rate_limit(request)
|
|
102
101
|
|
|
103
102
|
self.collection = await self.prepare_collection(request)
|
|
104
103
|
resource = await self.prepare_resource(request)
|
|
@@ -142,11 +141,6 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
142
141
|
raise APIError.UNAUTHORIZED()
|
|
143
142
|
return auth
|
|
144
143
|
|
|
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
144
|
# Prepare data
|
|
151
145
|
# ------------
|
|
152
146
|
@abc.abstractmethod
|
|
@@ -240,12 +234,10 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
240
234
|
@overload
|
|
241
235
|
async def dump( # type: ignore[misc]
|
|
242
236
|
self, request, data: TVData, *, many: Literal[True]
|
|
243
|
-
) -> list[TSchemaRes]:
|
|
244
|
-
...
|
|
237
|
+
) -> list[TSchemaRes]: ...
|
|
245
238
|
|
|
246
239
|
@overload
|
|
247
|
-
async def dump(self, request, data: TVData, *, many: bool = False) -> TSchemaRes:
|
|
248
|
-
...
|
|
240
|
+
async def dump(self, request, data: TVData, *, many: bool = False) -> TSchemaRes: ...
|
|
249
241
|
|
|
250
242
|
async def dump(
|
|
251
243
|
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/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.0.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,8 +2,8 @@ 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=
|
|
5
|
+
muffin_rest/handler.py,sha256=p3YQZRF2XgoBrf3r2QNVECqjTilYk5ZXFQ53GiJpZAA,10450
|
|
6
|
+
muffin_rest/limits.py,sha256=Fnlu4Wj3B-BzpahLK-rDbd1GEd7CEQ3zxyOD0vee7GE,2007
|
|
7
7
|
muffin_rest/marshmallow.py,sha256=jWsZeMj3KslbGGgzEMo8-e5eeuGhmqFolL0mIEquylc,789
|
|
8
8
|
muffin_rest/mongo/__init__.py,sha256=SiYSbX6ySJl43fw9aGREIs8ZsS8Qk_ieizoPOj4DjJc,4656
|
|
9
9
|
muffin_rest/mongo/filters.py,sha256=yIxIDVqMn6SoDgVhCqiTxYetw0hoaf_3jIvX2Vnizok,964
|
|
@@ -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.0.0.dist-info/LICENSE,sha256=xHPkOZhjyKBMOwXpWn9IB_BVLjrrMxv2M9slKkHj2hM,1082
|
|
37
|
+
muffin_rest-10.0.0.dist-info/METADATA,sha256=zVOQR6PAyL4QD4P8S1HPS3g5CPMfCpR3S-EGKCnsBpg,4147
|
|
38
|
+
muffin_rest-10.0.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
39
|
+
muffin_rest-10.0.0.dist-info/RECORD,,
|
|
File without changes
|