muffin-rest 9.3.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/filters.py CHANGED
@@ -1,17 +1,20 @@
1
1
  """Support API filters."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import operator
5
6
  from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping, Optional # py39
6
7
 
7
8
  import marshmallow as ma
8
- from asgi_tools._compat import json_loads # type: ignore[]
9
+ from asgi_tools._compat import json_loads
9
10
 
10
11
  from .utils import Mutate, Mutator
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from muffin import Request
14
15
 
16
+ from muffin_rest.types import TFilterOps, TFilterValue
17
+
15
18
  from .types import TVCollection
16
19
 
17
20
  FILTERS_PARAM = "where"
@@ -87,7 +90,7 @@ class Filter(Mutate):
87
90
 
88
91
  return ops, collection
89
92
 
90
- async def filter(self, collection, *ops: tuple[Callable, Any], **_) -> Any:
93
+ async def filter(self, collection, *ops: TFilterValue) -> Any:
91
94
  """Apply the filter to collection."""
92
95
 
93
96
  def validator(obj):
@@ -95,12 +98,22 @@ class Filter(Mutate):
95
98
 
96
99
  return [item for item in collection if validator(item)]
97
100
 
98
- def parse(self, data: Mapping):
101
+ def get_simple_value(self, ops: TFilterOps) -> Any:
102
+ """Get simple value from filter's data.
103
+
104
+ In case of simple filter, return the value.
105
+ """
106
+ if not ops:
107
+ return None
108
+
109
+ return ops[0][1]
110
+
111
+ def parse(self, data: Mapping) -> TFilterOps:
99
112
  """Parse operator and value from filter's data."""
100
113
  value = data.get(self.name, ma.missing)
101
114
  return tuple(self._parse(value))
102
115
 
103
- def _parse(self, value):
116
+ def _parse(self, value) -> Iterable[TFilterValue]:
104
117
  deserialize = self.schema_field.deserialize
105
118
  if isinstance(value, dict):
106
119
  for op, val in value.items():
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
- 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,8 +1,9 @@
1
1
  """Support filters for Mongo."""
2
2
 
3
- from typing import Any, Callable, ClassVar
3
+ from typing import ClassVar
4
4
 
5
5
  from muffin_rest.filters import Filter, Filters
6
+ from muffin_rest.types import TFilterValue
6
7
 
7
8
 
8
9
  class MongoFilter(Filter):
@@ -21,7 +22,7 @@ class MongoFilter(Filter):
21
22
  "$ends": lambda _, v: ("$regex", f"{ v }$"),
22
23
  }
23
24
 
24
- async def filter(self, collection, *ops: tuple[Callable, Any], **_):
25
+ async def filter(self, collection, *ops: TFilterValue):
25
26
  """Apply the filter."""
26
27
  return collection.find({self.field: dict(op(self.name, v) for op, v in ops)})
27
28
 
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,9 +1,10 @@
1
1
  """Support filters for Peewee ORM."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import operator
5
6
  from functools import reduce
6
- from typing import Any, Callable, ClassVar, Union, cast
7
+ from typing import TYPE_CHECKING, ClassVar, Union, cast
7
8
 
8
9
  from peewee import ColumnBase, Field, ModelSelect
9
10
 
@@ -11,6 +12,9 @@ from muffin_rest.filters import Filter, Filters
11
12
 
12
13
  from .utils import get_model_field_by_name
13
14
 
15
+ if TYPE_CHECKING:
16
+ from muffin_rest.types import TFilterValue
17
+
14
18
 
15
19
  class PWFilter(Filter):
16
20
  """Support Peewee."""
@@ -33,9 +37,7 @@ class PWFilter(Filter):
33
37
 
34
38
  list_ops = (*Filter.list_ops, "$between")
35
39
 
36
- async def filter(
37
- self, collection: ModelSelect, *ops: tuple[Callable, Any], **kwargs
38
- ) -> ModelSelect:
40
+ async def filter(self, collection: ModelSelect, *ops: TFilterValue) -> ModelSelect:
39
41
  """Apply the filters to Peewee QuerySet.."""
40
42
  column = self.field
41
43
  if isinstance(column, ColumnBase):
@@ -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)
@@ -2,13 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Callable, ClassVar, Union, cast
5
+ from typing import TYPE_CHECKING, Any, ClassVar, Union, cast
6
6
 
7
7
  from sqlalchemy import Column
8
8
 
9
9
  from muffin_rest.filters import Filter, Filters
10
10
 
11
11
  if TYPE_CHECKING:
12
+ from muffin_rest.types import TFilterValue
13
+
12
14
  from .types import TVCollection
13
15
 
14
16
 
@@ -29,17 +31,15 @@ class SAFilter(Filter):
29
31
 
30
32
  list_ops = (*Filter.list_ops, "$between")
31
33
 
32
- async def filter(
33
- self, collection: TVCollection, *ops: tuple[Callable, Any], **kwargs
34
- ) -> TVCollection:
34
+ async def filter(self, collection: TVCollection, *ops: TFilterValue) -> TVCollection:
35
35
  """Apply the filters to SQLAlchemy Select."""
36
36
  column = self.field
37
37
  if ops and column is not None:
38
- return self.query(collection, column, *ops, **kwargs)
38
+ return self.query(collection, column, *ops)
39
39
 
40
40
  return collection
41
41
 
42
- def query(self, select: TVCollection, column: Column, *ops, **_) -> TVCollection:
42
+ def query(self, select: TVCollection, column: Column, *ops: TFilterValue) -> TVCollection:
43
43
  """Filter a select."""
44
44
  return select.where(*[op(column, val) for op, val in ops])
45
45
 
muffin_rest/types.py CHANGED
@@ -19,3 +19,6 @@ TAuth = Callable[[Request], Awaitable]
19
19
  TVAuth = TypeVar("TVAuth", bound=TAuth)
20
20
  TVHandler = TypeVar("TVHandler", bound=type["RESTBase"])
21
21
  TSchemaRes = dict[str, Any]
22
+
23
+ TFilterValue = tuple[Callable, Any]
24
+ TFilterOps = tuple[TFilterValue, ...]
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: muffin-rest
3
- Version: 9.3.0
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
 
@@ -1,39 +1,39 @@
1
1
  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
- muffin_rest/filters.py,sha256=MARP_WsJslncahlgZLTImigl0RTxBtb0jmZbXfJ0c6Y,5637
5
- muffin_rest/handler.py,sha256=3efL8-AbxR0oHEwySfWSyfRuxCWyJOwDaYAhUzg6f-0,10772
6
- muffin_rest/limits.py,sha256=pA5hnDQgrP-euDGjAoczlT_b7Dxzw5btQ-3okkHYKSA,1855
4
+ muffin_rest/filters.py,sha256=hc-fhBODwrgJM_CvXNtTVH6jIOkjLog8En0iKKYXAGU,5947
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
- muffin_rest/mongo/filters.py,sha256=4TgExaUhJiHkogiZLqjtT13A0O-Uvwfd5AesVfTY2Io,949
9
+ muffin_rest/mongo/filters.py,sha256=yIxIDVqMn6SoDgVhCqiTxYetw0hoaf_3jIvX2Vnizok,964
10
10
  muffin_rest/mongo/schema.py,sha256=y4OEPQnlV_COTIIQ3cKmpqDpD2r18eAWn0rijQldWm0,1205
11
11
  muffin_rest/mongo/sorting.py,sha256=iJBnaFwE7g_JMwpGpQkoqSqbQK9XULx1K3skiRRgLgY,870
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
- muffin_rest/peewee/filters.py,sha256=oghjKwurNCyFUYT0r2TVu2Nd1SIalRsmbU4_RbaoXLs,2440
17
+ muffin_rest/peewee/filters.py,sha256=p813eJqyTkAhmS3C1P8rWFWb9Tl33OtADjgLctqKnns,2475
18
18
  muffin_rest/peewee/handler.py,sha256=Tk20ChTGkhzSF0K1-TFruPfXJyHfJE_fDKcQqGQeoAU,5297
19
19
  muffin_rest/peewee/openapi.py,sha256=lDnLnoXi33p0YeFVwRgaVrndyrG2XL93RH-BzbxinOY,1105
20
20
  muffin_rest/peewee/options.py,sha256=TimJtErC9e8B7BRiEkHiBZd71_bZbYr-FE2PIlQvfH0,1455
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
28
28
  muffin_rest/sorting.py,sha256=7k7dxElnEGiwvdfDivzcoLnAOXqpQoK52H-ss79Gw0g,2813
29
29
  muffin_rest/sqlalchemy/__init__.py,sha256=9MSvOXWP6665LiA5O1Icl2V05bpz6Ex5sUHB9YWKLvE,6393
30
- muffin_rest/sqlalchemy/filters.py,sha256=oVz3he9P_KiYE4fM3Wjka2IUmhkf4gHDOVRzKlz0SA0,2484
30
+ muffin_rest/sqlalchemy/filters.py,sha256=ejqjEZU0vqvnLwG0sX7QV3RjcXI0a2Ro_A34AAiGMEc,2489
31
31
  muffin_rest/sqlalchemy/sorting.py,sha256=YlFKpIet4TUy7fJ2UBLC8b9lAOwY66QBpPDDApbyh8M,1643
32
32
  muffin_rest/sqlalchemy/types.py,sha256=Exm-zAQCtPAwXvYcCTtPRqSa-wTEWRcH_v2YSsJkB6s,198
33
33
  muffin_rest/swagger.html,sha256=2uGLu_KpkYf925KnDKHBJmV9pm6OHn5C3BWScESsUS8,1736
34
- muffin_rest/types.py,sha256=lU0tAaDu9sKm-30RM1_qOx-hOYEkNRaA72LdWDvg72g,491
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.3.0.dist-info/LICENSE,sha256=xHPkOZhjyKBMOwXpWn9IB_BVLjrrMxv2M9slKkHj2hM,1082
37
- muffin_rest-9.3.0.dist-info/METADATA,sha256=Qhc7G-Y0XlNe6A-tq1cI4zkH_VzS9NY0RRcXbRO-hkg,4134
38
- muffin_rest-9.3.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
39
- muffin_rest-9.3.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.0.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any