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 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
- return cast(
237
- TVData[TVResource], await load_data(request, schema, partial=resource is not None)
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
- if key not in RATE_LIMITS:
34
- RATE_LIMITS[key] = (now, 1)
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 = RATE_LIMITS[key]
41
+ last, count = storage[key]
38
42
  if now - last > self.period:
39
- RATE_LIMITS[key] = (now, 1)
43
+ storage[key] = (now, 1)
40
44
  return True
41
45
 
42
46
  if count < self.limit:
43
- RATE_LIMITS[key] = (last, count + 1)
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, period: int, *, redis, **opts):
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, period)
66
+ super().__init__(limit, **opts)
63
67
  self.redis = redis
64
68
 
65
69
  async def check(self, key: str) -> bool:
@@ -1,26 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Mapping
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 asgi_tools import Request
10
+ from collections.abc import Mapping
12
11
 
13
12
 
14
- async def load_data(request: Request, schema: Optional[Schema] = None, **params):
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(cast(Union[Mapping, list], data), many=isinstance(data, list), **params)
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 Any, ClassVar
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(
@@ -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
1
+ Metadata-Version: 2.3
2
2
  Name: muffin-rest
3
- Version: 9.4.0
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=3efL8-AbxR0oHEwySfWSyfRuxCWyJOwDaYAhUzg6f-0,10772
6
- muffin_rest/limits.py,sha256=pA5hnDQgrP-euDGjAoczlT_b7Dxzw5btQ-3okkHYKSA,1855
7
- muffin_rest/marshmallow.py,sha256=jWsZeMj3KslbGGgzEMo8-e5eeuGhmqFolL0mIEquylc,789
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=reHbd2o-F6bKEKc8bznzj0TMY2vzjK6Yt1qOny7kt_w,2691
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=wXeneVE1IZl1ROnY28re73H62Y1_tEmoEQYzPhuOyBI,702
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-9.4.0.dist-info/LICENSE,sha256=xHPkOZhjyKBMOwXpWn9IB_BVLjrrMxv2M9slKkHj2hM,1082
37
- muffin_rest-9.4.0.dist-info/METADATA,sha256=7LF2JbYYQ4k9VusfwGZolZi9oY3rHNIIC3TQs03tUFM,4134
38
- muffin_rest-9.4.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
39
- muffin_rest-9.4.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any