Skip to content

Commit d8c8c45

Browse files
authored
Merge pull request #153 from eadwinCode/ninja_1_2_0_support
feat: Ninja V1.2.0 Support
2 parents fdf8edd + 3b20123 commit d8c8c45

File tree

17 files changed

+423
-401
lines changed

17 files changed

+423
-401
lines changed

docs/tutorial/throttling.md

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ Throttling can be seen as a permission that determines if a request should be au
44
It indicates a temporary state used to control the rate of requests that clients can make to an API.
55

66
```python
7-
from ninja_extra import NinjaExtraAPI, throttle
7+
from ninja_extra import NinjaExtraAPI
8+
from ninja_extra.throttling import UserRateThrottle, AnonRateThrottle
89
api = NinjaExtraAPI()
910

10-
@api.get('/users')
11-
@throttle # this will apply default throttle classes [UserRateThrottle, AnonRateThrottle]
11+
@api.get('/users', throttle=[AnonRateThrottle(), UserRateThrottle()])
1212
def my_throttled_endpoint(request):
1313
return 'foo'
1414
```
@@ -23,7 +23,7 @@ constraints, which could be burst throttling rate or sustained throttling rates,
2323
for example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
2424

2525
```python
26-
from ninja_extra import NinjaExtraAPI, throttle
26+
from ninja_extra import NinjaExtraAPI
2727
from ninja_extra.throttling import UserRateThrottle
2828
api = NinjaExtraAPI()
2929

@@ -36,8 +36,7 @@ class User1000PerDayRateThrottle(UserRateThrottle):
3636
rate = "1000/day"
3737
scope = "days"
3838

39-
@api.get('/users')
40-
@throttle(User60MinRateThrottle, User1000PerDayRateThrottle)
39+
@api.get('/users', throttle=[User60MinRateThrottle(), User1000PerDayRateThrottle()])
4140
def my_throttled_endpoint(request):
4241
return 'foo'
4342

@@ -61,13 +60,12 @@ NINJA_EXTRA = {
6160
The rate descriptions used in `THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
6261

6362
```python
64-
from ninja_extra import NinjaExtraAPI, throttle
63+
from ninja_extra import NinjaExtraAPI
6564
from ninja_extra.throttling import UserRateThrottle
6665

6766
api = NinjaExtraAPI()
6867

69-
@api.get('/users')
70-
@throttle(UserRateThrottle)
68+
@api.get('/users', throttle=UserRateThrottle())
7169
def my_throttled_endpoint(request):
7270
return 'foo'
7371
```
@@ -162,17 +160,15 @@ NINJA_EXTRA = {
162160

163161
```python
164162
# api.py
165-
from ninja_extra import NinjaExtraAPI, throttle
163+
from ninja_extra import NinjaExtraAPI
166164
from ninja_extra.throttling import DynamicRateThrottle
167165
api = NinjaExtraAPI()
168166

169-
@api.get('/users')
170-
@throttle(DynamicRateThrottle, scope='burst')
167+
@api.get('/users', throttle=DynamicRateThrottle(scope='burst'))
171168
def get_users(request):
172169
return 'foo'
173170

174-
@api.get('/users/<int:id>')
175-
@throttle(DynamicRateThrottle, scope='sustained')
171+
@api.get('/users/<int:id>', throttle=DynamicRateThrottle(scope='sustained'))
176172
def get_user_by_id(request, id: int):
177173
return 'foo'
178174
```
@@ -187,21 +183,15 @@ Here, we dynamically applied `sustained` rates and `burst` rates to `get_users`
187183
```python
188184
# api.py
189185
from ninja_extra import (
190-
NinjaExtraAPI, throttle, api_controller, ControllerBase,
186+
NinjaExtraAPI, api_controller, ControllerBase,
191187
http_get
192188
)
193189
from ninja_extra.throttling import DynamicRateThrottle
194190
api = NinjaExtraAPI()
195191

196-
@api_controller("/throttled-controller")
192+
@api_controller("/throttled-controller", throttle=[DynamicRateThrottle(scope="sustained")])
197193
class ThrottlingControllerSample(ControllerBase):
198-
throttling_classes = [
199-
DynamicRateThrottle,
200-
]
201-
throttling_init_kwargs = dict(scope="sustained")
202-
203-
@http_get("/endpoint_1")
204-
@throttle(DynamicRateThrottle, scope='burst')
194+
@http_get("/endpoint_1", throttle=DynamicRateThrottle(scope="burst"))
205195
def endpoint_1(self, request):
206196
# this will override the generally throttling applied at the controller
207197
return "foo"
@@ -216,4 +206,4 @@ class ThrottlingControllerSample(ControllerBase):
216206

217207

218208
api.register_controllers(ThrottlingControllerSample)
219-
```
209+
```

ninja_extra/conf/settings.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ class Config:
4141
THROTTLE_RATES: Dict[str, Optional[str]] = Field(
4242
{"user": "1000/day", "anon": "100/day"}
4343
)
44-
THROTTLE_CLASSES: List[Any] = []
44+
THROTTLE_CLASSES: List[Any] = Field(
45+
[
46+
"ninja_extra.throttling.AnonRateThrottle",
47+
"ninja_extra.throttling.UserRateThrottle",
48+
]
49+
)
4550
NUM_PROXIES: Optional[int] = None
4651
INJECTOR_MODULES: List[Any] = []
4752
ORDERING_CLASS: Any = Field(

ninja_extra/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
TRACE = "TRACE"
1616
ROUTE_METHODS = [POST, PUT, PATCH, DELETE, GET, HEAD, OPTIONS, TRACE]
1717
THROTTLED_FUNCTION = "__throttled_endpoint__"
18+
THROTTLED_OBJECTS = "__throttled_objects__"
1819
ROUTE_FUNCTION = "__route_function__"
1920

2021
ROUTE_CONTEXT_VAR: contextvars.ContextVar[t.Optional["RouteContext"]] = (

ninja_extra/controllers/base.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
from django.urls import path as django_path
2727
from injector import inject, is_decorated_with_inject
2828
from ninja import NinjaAPI, Router
29-
from ninja.constants import NOT_SET
29+
from ninja.constants import NOT_SET, NOT_SET_TYPE
3030
from ninja.security.base import AuthBase
3131
from ninja.signature import is_async
32+
from ninja.throttling import BaseThrottle
3233
from ninja.utils import normalize_path
3334

34-
from ninja_extra.constants import ROUTE_FUNCTION, THROTTLED_FUNCTION
35+
from ninja_extra.constants import ROUTE_FUNCTION, THROTTLED_FUNCTION, THROTTLED_OBJECTS
3536
from ninja_extra.exceptions import APIException, NotFound, PermissionDenied, bad_request
3637
from ninja_extra.helper import get_function_name
3738
from ninja_extra.operation import Operation, PathView
@@ -51,7 +52,6 @@
5152

5253
if TYPE_CHECKING: # pragma: no cover
5354
from ninja_extra import NinjaExtraAPI
54-
from ninja_extra.throttling import BaseThrottle
5555

5656
from .route.context import RouteContext
5757

@@ -126,10 +126,10 @@ def some_method_name(self):
126126
throttling_classes: List[Type["BaseThrottle"]] = []
127127
throttling_init_kwargs: Optional[Dict[Any, Any]] = None
128128

129-
Ok = Ok
130-
Id = Id
131-
Detail = Detail
132-
bad_request = bad_request
129+
Ok = Ok # TODO: remove soonest
130+
Id = Id # TODO: remove soonest
131+
Detail = Detail # TODO: remove soonest
132+
bad_request = bad_request # TODO: remove soonest
133133

134134
@classmethod
135135
def get_api_controller(cls) -> "APIController":
@@ -294,6 +294,7 @@ def __init__(
294294
prefix: str,
295295
*,
296296
auth: Any = NOT_SET,
297+
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
297298
tags: Union[Optional[List[str]], str] = None,
298299
permissions: Optional["PermissionType"] = None,
299300
auto_import: bool = True,
@@ -303,6 +304,7 @@ def __init__(
303304
self.auth: Optional[AuthBase] = auth
304305

305306
self.tags = tags # type: ignore
307+
self.throttle = throttle
306308

307309
self.auto_import: bool = auto_import # set to false and it would be ignored when api.auto_discover is called
308310
# `controller_class` target class that the APIController wraps
@@ -348,8 +350,6 @@ def tags(self, value: Union[str, List[str], None]) -> None:
348350
self._tags = tag
349351

350352
def __call__(self, cls: ControllerClassType) -> ControllerClassType:
351-
from ninja_extra.throttling import throttle
352-
353353
self.auto_import = getattr(cls, "auto_import", self.auto_import)
354354
if not issubclass(cls, ControllerBase):
355355
# We force the cls to inherit from `ControllerBase` by creating another type.
@@ -360,8 +360,15 @@ def __call__(self, cls: ControllerClassType) -> ControllerClassType:
360360
assert isinstance(
361361
cls.throttling_classes, (list, tuple)
362362
), f"Controller[{cls.__name__}].throttling_class must be a list or tuple"
363-
has_throttling_classes = len(cls.throttling_classes) > 0
364-
throttling_init_kwargs = cls.throttling_init_kwargs or {}
363+
364+
throttling_objects: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE]
365+
if cls.throttling_classes:
366+
throttling_init_kwargs = cls.throttling_init_kwargs or {}
367+
throttling_objects = [
368+
item(**throttling_init_kwargs) for item in cls.throttling_classes
369+
]
370+
else:
371+
throttling_objects = self.throttle
365372

366373
if not self.tags:
367374
tag = str(cls.__name__).lower().replace("controller", "")
@@ -386,10 +393,13 @@ def __call__(self, cls: ControllerClassType) -> ControllerClassType:
386393

387394
for _, v in self._controller_class_route_functions.items():
388395
throttled_endpoint = v.as_view.__dict__.get(THROTTLED_FUNCTION)
389-
if not throttled_endpoint and has_throttling_classes:
390-
v.route.view_func = throttle(
391-
*cls.throttling_classes, **throttling_init_kwargs
392-
)(v.route.view_func)
396+
397+
if throttled_endpoint or throttling_objects is not NOT_SET:
398+
v.route.route_params.throttle = v.as_view.__dict__.get(
399+
THROTTLED_OBJECTS, lambda: throttling_objects
400+
)()
401+
setattr(v.route.view_func, THROTTLED_FUNCTION, True)
402+
393403
self._add_operation_from_route_function(v)
394404

395405
if not is_decorated_with_inject(cls.__init__):
@@ -463,6 +473,7 @@ def add_api_operation(
463473
view_func: Callable,
464474
*,
465475
auth: Any = NOT_SET,
476+
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
466477
response: Any = NOT_SET,
467478
operation_id: Optional[str] = None,
468479
summary: Optional[str] = None,
@@ -504,13 +515,14 @@ def add_api_operation(
504515
url_name=url_name,
505516
include_in_schema=include_in_schema,
506517
openapi_extra=openapi_extra,
518+
throttle=throttle,
507519
)
508520
return operation
509521

510522

511523
@overload
512524
def api_controller(
513-
prefix_or_class: Type[T],
525+
prefix_or_class: Union[ControllerClassType, Type[T]],
514526
) -> Union[Type[ControllerBase], Type[T]]: # pragma: no cover
515527
...
516528

@@ -519,16 +531,20 @@ def api_controller(
519531
def api_controller(
520532
prefix_or_class: str = "",
521533
auth: Any = NOT_SET,
534+
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
522535
tags: Union[Optional[List[str]], str] = None,
523536
permissions: Optional["PermissionType"] = None,
524537
auto_import: bool = True,
525-
) -> Callable[[Type[T]], Union[Type[ControllerBase], Type[T]]]: # pragma: no cover
538+
) -> Callable[
539+
[Union[Type, Type[T]]], Union[Type[ControllerBase], Type[T]]
540+
]: # pragma: no cover
526541
...
527542

528543

529544
def api_controller(
530-
prefix_or_class: Union[str, ControllerClassType] = "",
545+
prefix_or_class: Union[str, Union[ControllerClassType, Type]] = "",
531546
auth: Any = NOT_SET,
547+
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
532548
tags: Union[Optional[List[str]], str] = None,
533549
permissions: Optional["PermissionType"] = None,
534550
auto_import: bool = True,
@@ -540,6 +556,7 @@ def api_controller(
540556
tags=tags,
541557
permissions=permissions,
542558
auto_import=auto_import,
559+
throttle=throttle,
543560
)(prefix_or_class)
544561

545562
def _decorator(cls: ControllerClassType) -> ControllerClassType:
@@ -549,6 +566,7 @@ def _decorator(cls: ControllerClassType) -> ControllerClassType:
549566
tags=tags,
550567
permissions=permissions,
551568
auto_import=auto_import,
569+
throttle=throttle,
552570
)(cls)
553571

554572
return _decorator

0 commit comments

Comments
 (0)