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 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
- base_property: str = "name"
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: muffin-rest
3
- Version: 7.3.5
3
+ Version: 8.0.0
4
4
  Summary: The package provides enhanced support for writing REST APIs with Muffin framework
5
5
  Home-page: https://github.com/klen/muffin-rest
6
6
  License: MIT
@@ -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=LcPFFH01nwe2tYKYbq-KVDX5UBVMlaRmeisJ_s4jsCo,10035
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=6K8YD3U-FdItFVjILQxH9h_Hwi68kgzWs97zrtSN_m4,2049
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-7.3.5.dist-info/LICENSE,sha256=xHPkOZhjyKBMOwXpWn9IB_BVLjrrMxv2M9slKkHj2hM,1082
36
- muffin_rest-7.3.5.dist-info/METADATA,sha256=3S9RVEQb7kpUDE8b1IWRZg6Q27bzmlIS0J7u5SpsRcI,4177
37
- muffin_rest-7.3.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
- muffin_rest-7.3.5.dist-info/RECORD,,
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,,