Skip to content

Commit 00d8fc5

Browse files
authored
Merge pull request #20 from eadwinCode/throttling
Throttling
2 parents 9bffd3e + 0b09175 commit 00d8fc5

File tree

18 files changed

+1381
-74
lines changed

18 files changed

+1381
-74
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,3 @@ You will see the automatic interactive API documentation (provided by <a href="h
116116
![Swagger UI](docs/docs/images/ui_swagger_preview_readme.gif)
117117
## What next?
118118
- To support this project, please give star it on Github
119-
- API Throttling
120-

docs/Makefile

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/docs/tutorial/throttling.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# **Throttling**
2+
3+
Throttling can be seen as a permission that determines if a request should be authorized.
4+
It indicates a temporary state used to control the rate of requests that clients can make to an API.
5+
6+
```python
7+
from ninja_extra import NinjaExtraAPI, throttle
8+
api = NinjaExtraAPI()
9+
10+
@api.get('/users')
11+
@throttle # this will apply default throttle classes [UserRateThrottle, AnonRateThrottle]
12+
def my_throttled_endpoint(request):
13+
return 'foo'
14+
```
15+
16+
!!! info
17+
The above example won't be throttled because the default scope for `UserRateThrottle` and `AnonRateThrottle`
18+
is `none`
19+
20+
## **Multiple Throttling**
21+
Django-ninja-extra throttle supposes multiple throttles which is useful to impose different
22+
constraints, which could be burst throttling rate or sustained throttling rates, on an API.
23+
for example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
24+
25+
```python
26+
from ninja_extra import NinjaExtraAPI, throttle
27+
from ninja_extra.throttling import UserRateThrottle
28+
api = NinjaExtraAPI()
29+
30+
class User60MinRateThrottle(UserRateThrottle):
31+
rate = "60/min"
32+
scope = "minutes"
33+
34+
35+
class User1000PerDayRateThrottle(UserRateThrottle):
36+
rate = "1000/day"
37+
scope = "days"
38+
39+
@api.get('/users')
40+
@throttle(User60MinRateThrottle, User1000PerDayRateThrottle)
41+
def my_throttled_endpoint(request):
42+
return 'foo'
43+
44+
```
45+
## **Throttling Policy Settings**
46+
You can set globally default throttling classes and rates in your project `settings.py` by overriding the keys below:
47+
```python
48+
# django settings.py
49+
NINJA_EXTRA = {
50+
'THROTTLE_CLASSES': [
51+
"ninja_extra.throttling.AnonRateThrottle",
52+
"ninja_extra.throttling.UserRateThrottle",
53+
],
54+
'THROTTLE_RATES': {
55+
'user': '1000/day',
56+
'anon': '100/day',
57+
},
58+
'NUM_PROXIES': None
59+
}
60+
```
61+
The rate descriptions used in `THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
62+
63+
```python
64+
from ninja_extra import NinjaExtraAPI, throttle
65+
from ninja_extra.throttling import UserRateThrottle
66+
67+
api = NinjaExtraAPI()
68+
69+
@api.get('/users')
70+
@throttle(UserRateThrottle)
71+
def my_throttled_endpoint(request):
72+
return 'foo'
73+
```
74+
75+
## **Clients Identification**
76+
Clients are identified by x-Forwarded-For in HTTP header and REMOTE_ADDR from WSGI variable.
77+
These are unique identities which identifies clients IP addresses used for throttling.
78+
`X-Forwarded-For` is preferable over `REMOTE_ADDR` and is used as so.
79+
80+
#### **Limit Clients Proxies**
81+
If you need to strictly identify unique client IP addresses, you'll need to first configure the number of application proxies that the API runs behind by setting the `NUM_PROXIES` setting. This setting should be an integer of zero or more.
82+
If set to non-zero then the client IP will be identified as being the last IP address in the X-Forwarded-For header, once any application proxy IP addresses have first been excluded. If set to zero, then the REMOTE_ADDR value will always be used as the identifying IP address.
83+
It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](https://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client.
84+
85+
!!! info
86+
Further context on how the X-Forwarded-For header works, and identifying a remote client IP can be found here.
87+
88+
## **Throttling Model Cache setup**
89+
The throttling models used in django-ninja-extra utilizes Django cache backend. It uses the `default` value of [`LocMemCache`]()
90+
See Django's [cache documentation](https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache) for more details.
91+
92+
If you dont want to use the default cache defined in throttle model, here is an example on how to define a different cache for a throttling model
93+
```python
94+
95+
from django.core.cache import caches
96+
from ninja_extra.throttling import AnonRateThrottle
97+
98+
99+
class CustomAnonRateThrottle(AnonRateThrottle):
100+
cache = caches['alternate']
101+
```
102+
# **API Reference**
103+
104+
## **AnonRateThrottle**
105+
`AnonRateThrottle` model is for throttling unauthenticated users using their IP address as key to throttle against.
106+
It is suitable for restricting rate of requests from an unknown source
107+
108+
Request Permission is determined by:
109+
- `rate` defined in derived class
110+
- `anon` scope defined in `THROTTLE_RATES` in `NINJA_EXTRA` settings in `settings.py`
111+
112+
## **UserRateThrottle**
113+
`UserRateThrottle` model is for throttling authenticated users using user id or pk to generate a key to throttle against.
114+
Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
115+
116+
Request Permission is determined by:
117+
- `rate` defined in derived class
118+
- `user` scope defined in `THROTTLE_RATES` in `NINJA_EXTRA` settings in `settings.py`
119+
120+
You can use multiple user throttle rates for a `UserRateThrottle` model, for example:
121+
```python
122+
# example/throttles.py
123+
from ninja_extra.throttling import UserRateThrottle
124+
125+
126+
class BurstRateThrottle(UserRateThrottle):
127+
scope = 'burst'
128+
129+
130+
class SustainedRateThrottle(UserRateThrottle):
131+
scope = 'sustained'
132+
```
133+
134+
```python
135+
# django settings.py
136+
NINJA_EXTRA = {
137+
'THROTTLE_CLASSES': [
138+
'example.throttles.BurstRateThrottle',
139+
'example.throttles.SustainedRateThrottle'
140+
],
141+
'THROTTLE_RATES': {
142+
'burst': '60/min',
143+
'sustained': '1000/day'
144+
}
145+
}
146+
```
147+
## **DynamicRateThrottle**
148+
`DynamicRateThrottle` model is for throttling authenticated and unauthenticated users in similar way as `UserRateThrottle`.
149+
Its key feature is in the ability to dynamically set `scope` where its used.
150+
for an example:
151+
we can defined a scope in settings
152+
153+
```python
154+
# django settings.py
155+
NINJA_EXTRA = {
156+
'THROTTLE_RATES': {
157+
'burst': '60/min',
158+
'sustained': '1000/day'
159+
}
160+
}
161+
```
162+
163+
```python
164+
# api.py
165+
from ninja_extra import NinjaExtraAPI, throttle
166+
from ninja_extra.throttling import DynamicRateThrottle
167+
api = NinjaExtraAPI()
168+
169+
@api.get('/users')
170+
@throttle(DynamicRateThrottle, scope='burst')
171+
def get_users(request):
172+
return 'foo'
173+
174+
@api.get('/users/<int:id>')
175+
@throttle(DynamicRateThrottle, scope='sustained')
176+
def get_user_by_id(request, id: int):
177+
return 'foo'
178+
```
179+
Here, we dynamically applied `sustained` rates and `burst` rates to `get_users` and `get_user_by_id` respectively
Lines changed: 60 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,60 @@
1-
site_name: Django Ninja Extra
2-
site_description: Django Ninja Extra - Adds more power to Django Ninja RESTful api library
3-
site_url: https://eadwincode.github.io/django-ninja-extra/
4-
repo_name: eadwinCode/django-ninja
5-
repo_url: https://github.com/eadwinCode/django-ninja-extra
6-
edit_uri: ''
7-
8-
theme:
9-
name: material
10-
palette:
11-
- media: "(prefers-color-scheme: light)"
12-
scheme: default
13-
primary: deeppurple
14-
accent: deeppurple
15-
toggle:
16-
icon: material/toggle-switch
17-
name: Switch to dark mode
18-
19-
- media: "(prefers-color-scheme: dark)"
20-
scheme: slate
21-
primary: blue
22-
accent: blue
23-
toggle:
24-
icon: material/toggle-switch-off-outline
25-
name: Switch to light mode
26-
27-
language: en
28-
icon:
29-
repo: fontawesome/brands/git-alt
30-
plugins:
31-
- search
32-
- mkdocstrings
33-
34-
nav:
35-
- Index: index.md
36-
- APIController:
37-
- Index: api_controller/index.md
38-
- Controller Routes: api_controller/api_controller_route.md
39-
- Controller Permissions: api_controller/api_controller_permission.md
40-
- Usage:
41-
- Quick Tutorial: tutorial/index.md
42-
- Authentication: tutorial/authentication.md
43-
- Path Parameter: tutorial/path.md
44-
- Query Request: tutorial/query.md
45-
- Body Request: tutorial/body_request.md
46-
- Form Request: tutorial/form.md
47-
- Schema: tutorial/schema.md
48-
- Pagination: tutorial/pagination.md
49-
- Error Handling: tutorial/custom_exception.md
50-
- Versioning: tutorial/versioning.md
51-
- Testing: tutorial/testing.md
52-
- Settings: settings.md
53-
- Dependency Injection: service_module_injector.md
54-
55-
markdown_extensions:
56-
- markdown_include.include
57-
- codehilite
58-
- admonition
59-
- pymdownx.superfences
1+
site_name: Django Ninja Extra
2+
site_description: Django Ninja Extra - Adds more power to Django Ninja RESTful api library
3+
site_url: https://eadwincode.github.io/django-ninja-extra/
4+
repo_name: eadwinCode/django-ninja
5+
repo_url: https://github.com/eadwinCode/django-ninja-extra
6+
edit_uri: ''
7+
8+
theme:
9+
name: material
10+
palette:
11+
- media: "(prefers-color-scheme: light)"
12+
scheme: default
13+
primary: deeppurple
14+
accent: deeppurple
15+
toggle:
16+
icon: material/toggle-switch
17+
name: Switch to dark mode
18+
19+
- media: "(prefers-color-scheme: dark)"
20+
scheme: slate
21+
primary: blue
22+
accent: blue
23+
toggle:
24+
icon: material/toggle-switch-off-outline
25+
name: Switch to light mode
26+
27+
language: en
28+
icon:
29+
repo: fontawesome/brands/git-alt
30+
plugins:
31+
- search
32+
- mkdocstrings
33+
34+
nav:
35+
- Index: index.md
36+
- APIController:
37+
- Index: api_controller/index.md
38+
- Controller Routes: api_controller/api_controller_route.md
39+
- Controller Permissions: api_controller/api_controller_permission.md
40+
- Usage:
41+
- Quick Tutorial: tutorial/index.md
42+
- Authentication: tutorial/authentication.md
43+
- Path Parameter: tutorial/path.md
44+
- Query Request: tutorial/query.md
45+
- Body Request: tutorial/body_request.md
46+
- Form Request: tutorial/form.md
47+
- Schema: tutorial/schema.md
48+
- Pagination: tutorial/pagination.md
49+
- Error Handling: tutorial/custom_exception.md
50+
- Versioning: tutorial/versioning.md
51+
- Throttling: tutorial/throttling.md
52+
- Testing: tutorial/testing.md
53+
- Settings: settings.md
54+
- Dependency Injection: service_module_injector.md
55+
56+
markdown_extensions:
57+
- markdown_include.include
58+
- codehilite
59+
- admonition
60+
- pymdownx.superfences

ninja_extra/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"""
22

3-
__version__ = "0.15.2"
3+
__version__ = "0.15.4"
44

55
import django
66

@@ -17,7 +17,9 @@
1717
from ninja_extra.controllers.route import route
1818
from ninja_extra.dependency_resolver import get_injector, service_resolver
1919
from ninja_extra.main import NinjaExtraAPI
20+
from ninja_extra.pagination import paginate
2021
from ninja_extra.router import Router
22+
from ninja_extra.throttling import throttle
2123

2224
if django.VERSION < (3, 2): # pragma: no cover
2325
default_app_config = "ninja_extra.apps.NinjaExtraConfig"
@@ -42,4 +44,6 @@
4244
"service_resolver",
4345
"lazy",
4446
"Router",
47+
"throttle",
48+
"paginate",
4549
]

ninja_extra/conf/settings.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, List
1+
from typing import Any, Dict, List, Optional
22

33
from django.conf import settings as django_settings
44
from django.test.signals import setting_changed
@@ -16,6 +16,11 @@ def __init__(self, data: dict) -> None:
1616
NinjaExtra_SETTINGS_DEFAULTS = dict(
1717
INJECTOR_MODULES=[],
1818
PAGINATION_CLASS="ninja_extra.pagination.LimitOffsetPagination",
19+
THROTTLE_CLASSES=[
20+
"ninja_extra.throttling.AnonRateThrottle",
21+
"ninja_extra.throttling.UserRateThrottle",
22+
],
23+
THROTTLE_RATES={"user": None, "anon": None},
1924
)
2025

2126
USER_SETTINGS = UserDefinedSettingsMapper(
@@ -32,6 +37,11 @@ class Config:
3237
"ninja_extra.pagination.LimitOffsetPagination",
3338
)
3439
PAGINATION_PER_PAGE: int = Field(100)
40+
THROTTLE_RATES: Dict[str, Optional[str]] = Field(
41+
{"user": "1000/day", "anon": "100/day"}
42+
)
43+
THROTTLE_CLASSES: List[Any] = []
44+
NUM_PROXIES: Optional[int] = None
3545
INJECTOR_MODULES: List[Any] = []
3646

3747
@validator("INJECTOR_MODULES", pre=True)
@@ -40,6 +50,12 @@ def pre_injector_module_validate(cls, value: Any) -> Any:
4050
raise ValueError("Invalid data type")
4151
return value
4252

53+
@validator("THROTTLE_CLASSES", pre=True)
54+
def pre_throttling_class_validate(cls, value: Any) -> Any:
55+
if not isinstance(value, list):
56+
raise ValueError("Invalid data type")
57+
return value
58+
4359
@validator("PAGINATION_CLASS", pre=True)
4460
def pre_pagination_class_validate(cls, value: Any) -> Any:
4561
if isinstance(value, list):

0 commit comments

Comments
 (0)