muffin-rest 7.3.5__py3-none-any.whl → 8.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 +10 -5
- muffin_rest/limits.py +73 -0
- muffin_rest/options.py +28 -2
- {muffin_rest-7.3.5.dist-info → muffin_rest-8.0.0.dist-info}/METADATA +1 -1
- {muffin_rest-7.3.5.dist-info → muffin_rest-8.0.0.dist-info}/RECORD +7 -6
- {muffin_rest-7.3.5.dist-info → muffin_rest-8.0.0.dist-info}/LICENSE +0 -0
- {muffin_rest-7.3.5.dist-info → muffin_rest-8.0.0.dist-info}/WHEEL +0 -0
muffin_rest/handler.py
CHANGED
|
@@ -87,10 +87,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
87
87
|
else:
|
|
88
88
|
router.bind(cls, f"/{ cls.meta.name }", methods=methods, **params)
|
|
89
89
|
router.bind(
|
|
90
|
-
cls,
|
|
91
|
-
f"/{ cls.meta.name }/{{{ cls.meta.name_id }}}",
|
|
92
|
-
methods=methods,
|
|
93
|
-
**params,
|
|
90
|
+
cls, f"/{ cls.meta.name }/{{{ cls.meta.name_id }}}", methods=methods, **params
|
|
94
91
|
)
|
|
95
92
|
|
|
96
93
|
for _, method in inspect.getmembers(cls, lambda m: hasattr(m, "__route__")):
|
|
@@ -102,14 +99,17 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
102
99
|
async def __call__(self, request: Request, *, method_name: Optional[str] = None, **_) -> Any:
|
|
103
100
|
"""Dispatch the given request by HTTP method."""
|
|
104
101
|
method = getattr(self, method_name or request.method.lower())
|
|
102
|
+
meta = self.meta
|
|
105
103
|
self.auth = await self.authorize(request)
|
|
104
|
+
if meta.rate_limit:
|
|
105
|
+
await self.rate_limit(request)
|
|
106
|
+
|
|
106
107
|
self.collection = await self.prepare_collection(request)
|
|
107
108
|
resource = await self.prepare_resource(request)
|
|
108
109
|
if not (request.method == "GET" and resource is None):
|
|
109
110
|
return await method(request, resource=resource)
|
|
110
111
|
|
|
111
112
|
headers = None
|
|
112
|
-
meta = self.meta
|
|
113
113
|
|
|
114
114
|
# Filter collection
|
|
115
115
|
if meta.filters:
|
|
@@ -148,6 +148,11 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
|
|
|
148
148
|
raise APIError.UNAUTHORIZED()
|
|
149
149
|
return auth
|
|
150
150
|
|
|
151
|
+
async def rate_limit(self, request: Request):
|
|
152
|
+
"""Default rate limit method. Proxy rate limit to self.api."""
|
|
153
|
+
if not await self.meta.rate_limiter.check(f"{self.auth}"):
|
|
154
|
+
raise APIError.TOO_MANY_REQUESTS()
|
|
155
|
+
|
|
151
156
|
# Prepare data
|
|
152
157
|
# ------------
|
|
153
158
|
@abc.abstractmethod
|
muffin_rest/limits.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from time import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RateLimiter(abc.ABC):
|
|
6
|
+
"""Rate limiter."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, limit: int, period: int, **opts):
|
|
9
|
+
"""Initialize the rate limiter.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
limit (int): The limit of requests.
|
|
13
|
+
period (int): The period of time in seconds.
|
|
14
|
+
"""
|
|
15
|
+
self.limit = limit
|
|
16
|
+
self.period = period
|
|
17
|
+
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
async def check(self, key: str) -> bool:
|
|
20
|
+
"""Check the request."""
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
RATE_LIMITS = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MemoryRateLimiter(RateLimiter):
|
|
28
|
+
"""Memory rate limiter. Do not use in production."""
|
|
29
|
+
|
|
30
|
+
async def check(self, key: str) -> bool:
|
|
31
|
+
"""Check the request."""
|
|
32
|
+
now = time()
|
|
33
|
+
if key not in RATE_LIMITS:
|
|
34
|
+
RATE_LIMITS[key] = (now, 1)
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
last, count = RATE_LIMITS[key]
|
|
38
|
+
if now - last > self.period:
|
|
39
|
+
RATE_LIMITS[key] = (now, 1)
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
if count < self.limit:
|
|
43
|
+
RATE_LIMITS[key] = (last, count + 1)
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RedisRateLimiter(RateLimiter):
|
|
50
|
+
"""Redis rate limiter."""
|
|
51
|
+
|
|
52
|
+
# TODO: Asyncio lock
|
|
53
|
+
|
|
54
|
+
def __init__(self, limit: int, period: int, *, redis, **opts):
|
|
55
|
+
"""Initialize the rate limiter.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
limit (int): The limit of requests.
|
|
59
|
+
period (int): The period of time in seconds.
|
|
60
|
+
redis (aioredis.Redis): The Redis connection.
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(limit, period)
|
|
63
|
+
self.redis = redis
|
|
64
|
+
|
|
65
|
+
async def check(self, key: str) -> bool:
|
|
66
|
+
"""Check the request."""
|
|
67
|
+
value = await self.redis.get(key)
|
|
68
|
+
if value is None:
|
|
69
|
+
await self.redis.setex(key, self.period, 1)
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
await self.redis.incr(key)
|
|
73
|
+
return int(value) < self.limit
|
muffin_rest/options.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""REST Options."""
|
|
2
2
|
|
|
3
|
-
from typing import Dict, Type
|
|
3
|
+
from typing import Any, Dict, Type
|
|
4
4
|
|
|
5
5
|
import marshmallow as ma
|
|
6
6
|
|
|
7
|
+
from muffin_rest.limits import MemoryRateLimiter, RateLimiter
|
|
8
|
+
|
|
7
9
|
from .filters import Filters
|
|
8
10
|
from .sorting import Sorting
|
|
9
11
|
|
|
@@ -13,6 +15,10 @@ class RESTOptions:
|
|
|
13
15
|
|
|
14
16
|
name: str = ""
|
|
15
17
|
name_id: str = "id"
|
|
18
|
+
base_property: str = "name"
|
|
19
|
+
|
|
20
|
+
# Pagination
|
|
21
|
+
# ----------
|
|
16
22
|
|
|
17
23
|
# limit: Paginate results (set to None for disable pagination)
|
|
18
24
|
limit: int = 0
|
|
@@ -23,14 +29,23 @@ class RESTOptions:
|
|
|
23
29
|
# limit_total: Return total count of results
|
|
24
30
|
limit_total: bool = True
|
|
25
31
|
|
|
32
|
+
# Filters
|
|
33
|
+
# -------
|
|
34
|
+
|
|
26
35
|
# Base class for filters
|
|
27
36
|
filters: Filters
|
|
28
37
|
filters_cls: Type[Filters] = Filters
|
|
29
38
|
|
|
39
|
+
# Sorting
|
|
40
|
+
# -------
|
|
41
|
+
|
|
30
42
|
# Base class for sorting
|
|
31
43
|
sorting: Sorting
|
|
32
44
|
sorting_cls: Type[Sorting] = Sorting
|
|
33
45
|
|
|
46
|
+
# Serialization/Deserialization
|
|
47
|
+
# -----------------------------
|
|
48
|
+
|
|
34
49
|
# Auto generation for schemas
|
|
35
50
|
Schema: Type[ma.Schema]
|
|
36
51
|
schema_base: Type[ma.Schema] = ma.Schema
|
|
@@ -38,7 +53,13 @@ class RESTOptions:
|
|
|
38
53
|
schema_meta: Dict = {}
|
|
39
54
|
schema_unknown: str = ma.EXCLUDE
|
|
40
55
|
|
|
41
|
-
|
|
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: Dict[str, Any] = {}
|
|
42
63
|
|
|
43
64
|
def __init__(self, cls):
|
|
44
65
|
"""Inherit meta options."""
|
|
@@ -65,6 +86,11 @@ class RESTOptions:
|
|
|
65
86
|
if not self.limit_max:
|
|
66
87
|
self.limit_max = self.limit
|
|
67
88
|
|
|
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
|
+
|
|
68
94
|
def setup_schema_meta(self, _):
|
|
69
95
|
"""Generate meta for schemas."""
|
|
70
96
|
return type(
|
|
@@ -2,7 +2,8 @@ muffin_rest/__init__.py,sha256=NBZeOEJgQHtFFhVgd9d0fpApFRgU405sbm0cu1y1MOU,1242
|
|
|
2
2
|
muffin_rest/api.py,sha256=gCqRb5PgKEMkE84Y0ZnJw_laVRmVWZRxzBqBv0ns6w8,3882
|
|
3
3
|
muffin_rest/errors.py,sha256=TIXSADZYSwx70dOVPRAzuNwGLfpLuzZZ1ugMZMwIGDo,1169
|
|
4
4
|
muffin_rest/filters.py,sha256=lMnPMdy7h0nB9bH-Nf5Hv_qenAGzG0V3ULpizTK3OnM,5623
|
|
5
|
-
muffin_rest/handler.py,sha256=
|
|
5
|
+
muffin_rest/handler.py,sha256=1v1I34K5DEuID3IQqwz4grsGruzXhdDTK7o7Ma0vAp0,10294
|
|
6
|
+
muffin_rest/limits.py,sha256=pA5hnDQgrP-euDGjAoczlT_b7Dxzw5btQ-3okkHYKSA,1855
|
|
6
7
|
muffin_rest/marshmallow.py,sha256=hHPLTLdaSz5jTLWBqyHeOwo2xfBv7aMIuJFD_trHRuE,715
|
|
7
8
|
muffin_rest/mongo/__init__.py,sha256=unoEAKCU9H3EKhQqKGosn02tTS3H5nPOcTd3THM4Qs8,4675
|
|
8
9
|
muffin_rest/mongo/filters.py,sha256=y2FleM_BqkICKGq3PmM_StOgKlE7RoSxt2NdQfCvnOE,921
|
|
@@ -11,7 +12,7 @@ muffin_rest/mongo/sorting.py,sha256=iJBnaFwE7g_JMwpGpQkoqSqbQK9XULx1K3skiRRgLgY,
|
|
|
11
12
|
muffin_rest/mongo/types.py,sha256=Otqu_FyIVnDAUGcwtzY_B77CKNBYApQPO_LlS2aLAQk,206
|
|
12
13
|
muffin_rest/mongo/utils.py,sha256=RZdAiKaTsgaFz8WbIPI_sjjVDsF_VmPu5Agzp5mpbvY,3946
|
|
13
14
|
muffin_rest/openapi.py,sha256=XNhU4EffbKvFKr1HMCqFM-ZrukPqLyZwm3aJe2dNs40,8770
|
|
14
|
-
muffin_rest/options.py,sha256=
|
|
15
|
+
muffin_rest/options.py,sha256=Ud8hYIEl9tFQ19n38JJy1QGgPRsivOC-2es-mDY5jc0,2663
|
|
15
16
|
muffin_rest/peewee/__init__.py,sha256=94DSj_ftT6fbPksHlBv40AH2HWaiZommUFOMN2jd9a4,129
|
|
16
17
|
muffin_rest/peewee/filters.py,sha256=dgbvTBm_V2Iu3penv5y--OWSysQ0ZnUfgGCuHcQKfhU,2433
|
|
17
18
|
muffin_rest/peewee/handler.py,sha256=O84-TmiyNyEhEamTh-DY93ds0hZJzVG8_yekRpF9p3k,5359
|
|
@@ -32,7 +33,7 @@ muffin_rest/sqlalchemy/types.py,sha256=JnIw44XJ2ClWzOv-mTUrvFw1JPxAlvdX_jf7r4zau
|
|
|
32
33
|
muffin_rest/swagger.html,sha256=2uGLu_KpkYf925KnDKHBJmV9pm6OHn5C3BWScESsUS8,1736
|
|
33
34
|
muffin_rest/types.py,sha256=vy55ShzMcvs9zXjFpdjWlagv09dMrcmxb2-U4hTL3NM,521
|
|
34
35
|
muffin_rest/utils.py,sha256=WT87AHXvBFBzBVTkwsYmDXgG3ZX1wNKFo4SOUJ9JiQY,2095
|
|
35
|
-
muffin_rest-
|
|
36
|
-
muffin_rest-
|
|
37
|
-
muffin_rest-
|
|
38
|
-
muffin_rest-
|
|
36
|
+
muffin_rest-8.0.0.dist-info/LICENSE,sha256=xHPkOZhjyKBMOwXpWn9IB_BVLjrrMxv2M9slKkHj2hM,1082
|
|
37
|
+
muffin_rest-8.0.0.dist-info/METADATA,sha256=VzP0M8MWFNqAo3klSmKO0e-C0946Ohnwmbj2rDPvk94,4177
|
|
38
|
+
muffin_rest-8.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
39
|
+
muffin_rest-8.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|