Skip to content

Commit d47fdd6

Browse files
authored
Merge pull request #58 from python-ellar/ellar_081_upgrade
Ellar 081 upgrade
2 parents 752333c + 537d64a commit d47fdd6

File tree

26 files changed

+344
-21
lines changed

26 files changed

+344
-21
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class AppModule:
4444

4545
```python
4646
from ellar.common import Controller, get
47-
from ellar_throttler import Throttle
47+
from ellar_throttler import Throttle, AnonymousThrottler, UserThrottler
4848
from ellar.di import injectable
4949

5050

@@ -63,7 +63,7 @@ class AppService:
6363
return {message: True}
6464

6565

66-
@Throttle(apply_interceptor=True)
66+
@Throttle(intercept=True)
6767
@Controller("/limit")
6868
class LimitController:
6969
def __init__(self, app_service: AppService):
@@ -77,6 +77,11 @@ class LimitController:
7777
@Throttle(anon={"limit": 3, "ttl": 5}, user={"limit": 3, "ttl": 3}) # overriding anon and user throttler config
7878
def get_shorter(self, use_auth: bool):
7979
return self.app_service.success(use_auth)
80+
81+
@get("/shorter-inline-throttling")
82+
@Throttle(AnonymousThrottler(ttl=5, limit=3), UserThrottler(ttl=3, limit=3)) # overriding global throttling options
83+
def get_shorter_inline_version(self, use_auth: bool):
84+
return self.app_service.success(use_auth)
8085
```
8186

8287
## References

ellar_throttler/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""A rate limiting module for Ellar"""
22

3-
__version__ = "0.1.5"
3+
__version__ = "0.1.7"
44

55
from .decorators import SkipThrottle, Throttle
66
from .exception import ThrottledException

ellar_throttler/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
THROTTLER_TTL = "THROTTLER:TTL"
33
THROTTLER_OPTIONS = "THROTTLER:MODULE_OPTIONS"
44
THROTTLER_SKIP = "THROTTLER:SKIP"
5+
INLINE_THROTTLERS = "INLINE_THROTTLERS:MODELS"

ellar_throttler/decorators.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from ellar.common import constants, set_metadata
44
from ellar.reflect import reflect
55

6-
from .constants import THROTTLER_LIMIT, THROTTLER_SKIP, THROTTLER_TTL
6+
from ellar_throttler.model.base import BaseThrottler
7+
8+
from .constants import INLINE_THROTTLERS, THROTTLER_LIMIT, THROTTLER_SKIP, THROTTLER_TTL
79
from .throttler_interceptor import ThrottlerInterceptor
810

911

@@ -14,7 +16,9 @@ class _Config(t.TypedDict):
1416
ttl: int
1517

1618

17-
def Throttle(*, apply_interceptor: bool = False, **kwargs: _Config) -> t.Callable:
19+
def Throttle(
20+
*throttlers: BaseThrottler, intercept: bool = False, **kwargs: _Config
21+
) -> t.Callable:
1822
"""
1923
Adds metadata to the target which will be handled by the ThrottlerGuard to
2024
handle incoming requests based on the given metadata.
@@ -24,11 +28,14 @@ def Throttle(*, apply_interceptor: bool = False, **kwargs: _Config) -> t.Callabl
2428
2529
@Throttle(throttler_model_name={'limit':20, 'ttl':300})
2630
class ControllerSample:
27-
@Throttle('user', limit=20, ttl=300)
31+
@Throttle(user={'limit':20, 'ttl':300})
2832
async def index(self):
2933
...
3034
31-
:param apply_interceptor: Indicates whether to apply ThrottlerInterceptor
35+
@Throttle(AnonymousThrottler(ttl=5, limit=3), UserThrottler(ttl=3, limit=3)) # overriding global throttling options
36+
class ControllerSample:
37+
pass
38+
:param intercept: Indicates whether to apply ThrottlerInterceptor
3239
:param kwargs: Throttler name used in setting up ThrottlerModule with overriding config
3340
:return: Callable
3441
"""
@@ -39,7 +46,10 @@ def decorator(func_: t.Callable) -> t.Callable:
3946
reflect.define_metadata(f"{THROTTLER_TTL}-{k}", v.get("ttl"), func_)
4047
reflect.define_metadata(f"{THROTTLER_LIMIT}-{k}", v.get("limit"), func_)
4148

42-
if apply_interceptor:
49+
if throttlers:
50+
reflect.define_metadata(INLINE_THROTTLERS, list(throttlers), func_)
51+
52+
if intercept:
4353
return set_metadata(constants.ROUTE_INTERCEPTORS, [ThrottlerInterceptor])( # type:ignore[no-any-return]
4454
func_
4555
)

ellar_throttler/model/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from ellar.utils import get_name
88
from starlette.responses import Response
99

10-
from ellar_throttler import IThrottlerStorage, ThrottledException
11-
from ellar_throttler.interfaces import IThrottleModel
10+
from ellar_throttler.exception import ThrottledException
11+
from ellar_throttler.interfaces import IThrottleModel, IThrottlerStorage
1212

1313

1414
class BaseThrottler(IThrottleModel, ABC):

ellar_throttler/module.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import functools
22
import typing as t
33

4+
from ellar.cache import CacheModule
45
from ellar.common import IExecutionContext, IModuleSetup, Module
6+
from ellar.common.exceptions import ImproperConfiguration
57
from ellar.core import Config, ModuleSetup
6-
from ellar.core.modules import DynamicModule, ModuleBase
8+
from ellar.core.modules import DynamicModule, ModuleBase, ModuleRefBase
79
from ellar.di import ProviderConfig
810

911
from ellar_throttler.interfaces import IThrottleModel, IThrottlerStorage
1012
from ellar_throttler.throttler_module_options import ThrottlerModuleOptions
11-
from ellar_throttler.throttler_service import ThrottlerStorageService
13+
from ellar_throttler.throttler_service import (
14+
CacheThrottlerStorageService,
15+
ThrottlerStorageService,
16+
)
1217

1318

1419
class _Config(t.TypedDict):
@@ -19,8 +24,23 @@ class _Config(t.TypedDict):
1924
skip_if: t.Optional[t.Callable[[IExecutionContext], bool]]
2025

2126

22-
@Module()
27+
@Module(name="ThrottlerModule", exports=[IThrottlerStorage, ThrottlerModuleOptions])
2328
class ThrottlerModule(ModuleBase, IModuleSetup):
29+
@classmethod
30+
def post_build(cls, module_ref: "ModuleRefBase") -> None:
31+
storage = module_ref.providers.get(IThrottlerStorage)
32+
if storage.use_class and ( # type:ignore[union-attr]
33+
storage.use_class == CacheThrottlerStorageService # type:ignore[union-attr]
34+
or issubclass(storage.use_class, CacheThrottlerStorageService) # type:ignore[union-attr]
35+
):
36+
try:
37+
module_ref.tree_manager.add_module_dependency(cls, CacheModule)
38+
except ValueError:
39+
raise ImproperConfiguration(
40+
f"Using {CacheThrottlerStorageService} as storage type requires {CacheModule} setup. "
41+
f"See https://python-ellar.github.io/ellar/security/rate-limit/"
42+
) from None
43+
2444
@classmethod
2545
def setup(
2646
cls,
@@ -66,7 +86,7 @@ def register_setup(cls, **override_config: t.Any) -> ModuleSetup:
6686

6787
@staticmethod
6888
def __register_setup_factory(
69-
module: t.Type["ThrottlerModule"],
89+
module_ref: ModuleRefBase,
7090
config: Config,
7191
override_config: t.Dict[str, t.Any],
7292
) -> DynamicModule:
@@ -81,7 +101,7 @@ def __register_setup_factory(
81101
schema = ThrottlerModuleOptions.model_validate(
82102
defined_config, from_attributes=True
83103
)
84-
104+
module = t.cast(t.Type[ThrottlerModule], module_ref.module)
85105
return module.__setup_module(schema, storage)
86106
raise RuntimeError(
87107
"Could not find `ELLAR_THROTTLER_CONFIG` in application config."

ellar_throttler/throttler_interceptor.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from ellar.core.services import Reflector
55
from ellar.di import injectable
66

7-
from .constants import THROTTLER_LIMIT, THROTTLER_SKIP, THROTTLER_TTL
7+
from .constants import INLINE_THROTTLERS, THROTTLER_LIMIT, THROTTLER_SKIP, THROTTLER_TTL
88
from .interfaces import IThrottlerStorage
99
from .throttler_module_options import ThrottlerModuleOptions
1010

@@ -37,7 +37,13 @@ async def intercept(
3737
if self.reflector.get_all_and_override(THROTTLER_SKIP, handler, class_ref):
3838
return await next_interceptor()
3939

40-
for throttler in self.options.throttlers:
40+
inline_throttlers = (
41+
self.reflector.get_all_and_override(INLINE_THROTTLERS, handler, class_ref)
42+
or []
43+
)
44+
throttlers = inline_throttlers or self.options.throttlers
45+
46+
for throttler in throttlers:
4147
# Continue if throttler was skipped
4248
skip_route_throttle = self.reflector.get_all_and_override(
4349
f"{THROTTLER_SKIP}-{throttler.name}", handler, class_ref

ellar_throttler/throttler_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ async def increment(self, key: str, ttl: int) -> ThrottlerStorageRecord:
7676

7777
@injectable()
7878
class CacheThrottlerStorageService(IThrottlerStorage):
79+
cache_backend: str = "default"
80+
7981
def __init__(self, cache_service: ICacheService) -> None:
8082
self._cache_service = cache_service
81-
self.cache_backend = "default"
8283

8384
@property
8485
def storage(self) -> t.Any: # pragma: no cover

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ classifiers = [
4040
]
4141

4242
dependencies = [
43-
"ellar >= 0.7.0"
43+
"ellar >= 0.8.2"
4444
]
4545

4646

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Introduction
2+
A simple project to demonstrate route throttling technique in ellar application using ellar-throttling package.
3+
4+
## Project setup
5+
```
6+
pip install -r requirements.txt
7+
```
8+
9+
## Development Server
10+
```
11+
python manage.py runserver --reload
12+
```
13+
Visit Swagger Docs: [http://localhost:8000/docs](http://localhost:8000/docs)

0 commit comments

Comments
 (0)