Skip to content

Commit b6a0423

Browse files
committed
Added ReadMe docs and LICENSE
1 parent 711897b commit b6a0423

File tree

3 files changed

+206
-1
lines changed

3 files changed

+206
-1
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Ezeudoh Tochukwu
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,188 @@
1111
[![PyPI version](https://img.shields.io/pypi/pyversions/ellar-throttler.svg)](https://pypi.python.org/pypi/ellar-throttler)
1212

1313
Full Documentation: [Here](https://eadwincode.github.io/ellar/throttling/)
14+
15+
## Introduction
16+
A rate limit module for Ellar
17+
18+
## Installation
19+
```shell
20+
$(venv) pip install ellar-throttler
21+
```
22+
## Usage
23+
### ThrottlerModule
24+
The `ThrottleModule` is the main entry point for this package, and can be used in a synchronous or asynchronous manner.
25+
All the needs to be passed is the `ttl`, the time to live in seconds for the request tracker, and the `limit`,
26+
or how many times an endpoint can be hit before returning a 429 status code.
27+
28+
```python
29+
from ellar.common import Module
30+
from ellar_throttler import ThrottlerModule
31+
32+
@Module(modules=[
33+
ThrottlerModule.setup(ttl=60, limit=10)
34+
])
35+
class ApplicationModule:
36+
pass
37+
```
38+
The above would mean that 10 requests from the same IP can be made to a single endpoint in 1 minute.
39+
40+
```python
41+
from ellar.common import Module
42+
from ellar_throttler import ThrottlerModule, ThrottlerGuard
43+
from ellar.core import Config, ModuleSetup, DynamicModule
44+
45+
def throttler_module_factory(module: ThrottlerModule, config: Config) -> DynamicModule:
46+
return module.setup(ttl=config['THROTTLE_TTL'], limit=config['THROTTLE_LIMIT'])
47+
48+
49+
@Module(modules=[
50+
ModuleSetup(ThrottlerModule, inject=[Config], factory=throttler_module_factory)
51+
])
52+
class ApplicationModule:
53+
pass
54+
55+
# server.py
56+
application = AppFactory.create_from_app_module(
57+
ApplicationModule,
58+
config_module=os.environ.get(
59+
ELLAR_CONFIG_MODULE, "dialerai.config:DevelopmentConfig"
60+
),
61+
global_guards=[ThrottlerGuard]
62+
)
63+
```
64+
The above is also a valid configuration for `ThrottleModule` registration if you want to work with config.
65+
66+
**NOTE**: If you add the `ThrottlerGuard` to your application `global_guards`, then all the incoming requests will be throttled by default.
67+
This can also be omitted in favor of `@guards(ThrottlerGuard)`.
68+
The global guard check can be skipped using the `@skip_throttle()` decorator mentioned later.
69+
70+
Example with `@guards(ThrottlerGuard)`
71+
```python
72+
# project_name/controller.py
73+
from ellar.common import Controller, guards
74+
from ellar_throttler import throttle, ThrottlerGuard, skip_throttle
75+
76+
@Controller()
77+
class AppController:
78+
79+
@guards(ThrottlerGuard)
80+
@throttle(limit=5, ttl=30)
81+
def normal(self):
82+
pass
83+
84+
```
85+
### Decorators
86+
#### @throttle()
87+
```
88+
@throttle(*, limit: int = 20, ttl: int = 60)
89+
```
90+
This decorator will set `THROTTLER_LIMIT` and `THROTTLER_TTL` metadata on the route, for retrieval from the Reflector class.
91+
It can be applied to controllers and routes.
92+
#### @skip_throttle()
93+
```
94+
@skip_throttle(skip: bool = True)
95+
```
96+
This decorator can be used to skip a route or a class or to negate the skipping of a route in
97+
a class that is skipped.
98+
99+
```python
100+
# project_name/controller.py
101+
from ellar.common import Controller
102+
from ellar_throttler import ThrottlerGuard, skip_throttle
103+
104+
@skip_throttle()
105+
@Controller(guards=[ThrottlerGuard])
106+
class AppController:
107+
108+
def do_skip(self):
109+
pass
110+
111+
@skip_throttle(skip=False)
112+
def dont_skip(self):
113+
pass
114+
```
115+
In the above controller, `dont_skip` would be counted against and
116+
rate-limited while `do_skip` would not be limited in any way.
117+
118+
### ThrottlerStorage
119+
Interface to define the methods to handle the details when it comes to keeping track of the requests.
120+
121+
Currently, the key is seen as an `MD5` hash of the IP the `class name` and the `function name`,
122+
to ensure that no unsafe characters are used.
123+
124+
The interface looks like this:
125+
126+
```python
127+
import typing as t
128+
from abc import ABC, abstractmethod
129+
130+
class IThrottlerStorage(ABC):
131+
@property
132+
@abstractmethod
133+
def storage(self) -> t.Dict[str, ThrottlerStorageOption]:
134+
"""
135+
The internal storage with all the request records.
136+
The key is a hashed key based on the current context and IP.
137+
:return:
138+
"""
139+
140+
@abstractmethod
141+
async def increment(self, key: str, ttl: int) -> ThrottlerStorageRecord:
142+
"""
143+
Increment the amount of requests for a given record. The record will
144+
automatically be removed from the storage once its TTL has been reached.
145+
:param key:
146+
:param ttl:
147+
:return:
148+
"""
149+
```
150+
So long as the Storage service implements this interface, it should be usable by the `ThrottlerGuard`.
151+
152+
### Proxies
153+
If you are working with multiple proxies, you can override the `get_tracker()` method to pull the value from the header or install
154+
[`ProxyHeadersMiddleware`](https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py)
155+
156+
```python
157+
# throttler_behind_proxy.guard.py
158+
from ellar_throttler import ThrottlerGuard
159+
from ellar.di import injectable
160+
from ellar.core.connection import HTTPConnection
161+
162+
163+
@injectable()
164+
class ThrottlerBehindProxyGuard(ThrottlerGuard):
165+
def get_tracker(self, connection: HTTPConnection) -> str:
166+
return connection.client.host # individualize IP extraction to meet your own needs
167+
168+
# project_name/controller.py
169+
from .throttler_behind_proxy import ThrottlerBehindProxyGuard
170+
171+
@Controller('', guards=[ThrottlerBehindProxyGuard])
172+
class AppController:
173+
pass
174+
```
175+
176+
### Working with WebSockets
177+
To work with Websockets you can extend the `ThrottlerGuard` and override the `handle_request` method with the code below:
178+
```python
179+
from ellar_throttler import ThrottlerGuard
180+
from ellar.di import injectable
181+
from ellar.core import IExecutionContext
182+
from ellar_throttler import ThrottledException
183+
184+
@injectable()
185+
class WsThrottleGuard(ThrottlerGuard):
186+
async def handle_request(self, context: IExecutionContext, limit: int, ttl: int) -> bool:
187+
websocket_client = context.switch_to_websocket().get_client()
188+
189+
host = websocket_client.client.host
190+
key = self.generate_key(context, host)
191+
result = await self.storage_service.increment(key, ttl)
192+
193+
# Throw an error when the user reached their limit.
194+
if result.total_hits > limit:
195+
raise ThrottledException(wait=result.time_to_expire)
196+
197+
return True
198+
```

ellar_throttler/throttler_guard.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ async def handle_request(
7171
self, context: IExecutionContext, limit: int, ttl: int
7272
) -> bool:
7373
connection, response = self.get_request_response(context)
74-
# TODO: Return early if the current user agent should be ignored.
7574

7675
tracker = self.get_tracker(connection)
7776
key = self.generate_key(context, tracker)

0 commit comments

Comments
 (0)