Django Ninja Extra

repository·master·Indexed 20 days ago

https://github.com/eadwincode/django-ninja-extra

An extension for Django Ninja that enhances REST API development by adding class-based controllers, built-in dependency injection, and an advanced permission system. It supports both synchronous and asynchronous permissions (via AsyncBasePermission), object-level access control, and the ability to combine permissions using logical operators (&, |, ~).

Tokens
39.9K
Snippets
105
Records
130
Agent score
69%

What's inside django-ninja-extra

  1. What is RouteContext and how to use it in controllers

    master

    The RouteContext is a central object in Django Ninja Extra that stores request-related information throughout the request lifecycle. It is automatically available within any class inheriting from ControllerBase via the self.context attribute.

    Key properties of RouteContext include:

    • request: The Django HttpRequest object.
    • response: The HttpResponse object being built.
    • permission_classes: A list of permission classes applied to the route.
    • args: Positional arguments passed to the route.
    • kwargs: Keyword arguments (route parameters) passed to the route.
    from ninja_extra import ControllerBase, api_controller, route
    
    @api_controller("/api")
    class MyController(ControllerBase):
        @route.get("/example")
        def my_method(self):
            # Accessing the context via self.context
            request = self.context.request
            return {"method": request.method}
  2. How path and query parameters are handled in ModelEndpointFactory

    master

    In ModelEndpointFactory, both path parameters (e.g., /{int:id}) and query parameters (e.g., ?query=int) are automatically parsed. These parameters are:

    1. Added as required fields to the Ninja input schema.
    2. Resolved during the request and passed as kwargs to the handler function.

    Ensure that the data types used in the path/query strings are compatible with Django URL converters (e.g., int, str, slug).

    # Path: /{int:id}/tags/{post_id}?query=int&query1=int
    # Generates kwargs: {'id': int, 'post_id': str, 'query': int, 'query1': int}
    
    list_post_tags = ModelEndpointFactory.list(
        path="/{int:id}/tags/{post_id}?query=int&query1=int",
        schema_out=model_config.retrieve_schema,
        queryset_getter=lambda self, **kw: self.list_post_tags_query(**kw)
    )
    
    def list_post_tags_query(self, **kwargs):
        # kwargs contains all path and query params
        post_id = kwargs['post_id']
        return Post.objects.filter(id=post_id).first().tags.all()
  3. Configure Model Controllers with ModelConfig

    master

    The ModelConfig class is used within a ModelControllerBase to define how a controller behaves. It centralizes configuration for the underlying Django model, schema generation, route availability, and pagination.

    Key parameters include:

    • model: The Django model being controlled.
    • schema_config: An instance of ModelSchemaConfig to customize auto-generated Pydantic schemas.
    • async_routes: Boolean to enable or disable async routes.
    • allowed_routes: A list of permitted operations (e.g., ['create', 'find_one', 'update', 'patch', 'delete', 'list']).
    • pagination: An instance of ModelPagination to configure list endpoint behavior.
    • create_schema, retrieve_schema, update_schema: Custom Pydantic schemas to use instead of auto-generated ones.
    from ninja_extra import ModelConfig, ModelControllerBase, api_controller
    from .models import Event
    
    @api_controller("/events")
    class EventModelController(ModelControllerBase):
        model_config = ModelConfig(
            model=Event,
            async_routes=False,
            allowed_routes=["create", "find_one", "update", "patch", "delete", "list"],
        )
  4. Use Ninja Schema for Django ORM to Pydantic conversion

    master

    If you are looking for a complete replacement for DRF Serializers, use ninja-schema. It converts Django ORM models into Pydantic schemas while supporting advanced Pydantic features.

    Key capabilities include:

    • Custom Field Support: Automatically converts Django model fields to native Pydantic types for immediate validation (e.g., Enums, EmailStr, IPv4Address, HttpUrl, JSON).
    • Field Validation: Supports field-level and model-level validation using Pydantic-style model_validator logic.
  5. Use Asynchronous Authentication Classes

    master

    Django Ninja Extra provides asynchronous versions of all standard authentication base classes in the ninja_extra.security package (e.g., AsyncHttpBearer).

    Requirements for Async Auth:

    1. Use an asynchronous auth class (e.g., AsyncHttpBearer) where the authenticate method is defined as async def.
    2. The endpoint handler (the function decorated with @route) must be an async def function.
    3. If an asynchronous auth class is applied at the api_controller level, all route handlers within that controller must be asynchronous.
    from ninja_extra import api_controller, route
    from ninja_extra.security import AsyncHttpBearer
    from ninja.constants import NOT_SET
    
    
    class AuthBearer(AsyncHttpBearer):
        async def authenticate(self, request, token):
            # await some actions
            if token == "supersecret":
                return token
    
    
    @api_controller(tags=['My Operations'], auth=NOT_SET, permissions=[])
    class MyController:
        @route.get("/bearer", auth=AuthBearer())
        async def bearer(self):
            return {"token": self.context.request.auth}
  6. How APIControllers and routers work together

    master

    In Django Ninja Extra, APIController classes are used to organize endpoints. The @api_controller decorator converts instance methods into API routes.

    To bridge these controllers with the underlying Django Ninja routing system, the ControllerRouter (often referred to via the router shorthand) acts as an adapter. This adapter converts APIController instances into a standard Django Ninja router, providing global control over all routes defined within those controller classes.

  7. Combine permissions with logical operators

    master

    Django Ninja Extra supports combining permissions using logical operators: & (AND), | (OR), and ~ (NOT). These operators create instances of AND, OR, or NOT classes that handle both synchronous and asynchronous permissions automatically.

    • AND (&): Both permissions must return True. Short-circuits on the first False.
    • OR (|): At least one permission must return True. Short-circuits on the first True.
    • NOT (~): Inverts the result of the permission.
    from ninja_extra import api_controller, http_get
    from ninja_extra.permissions import IsAuthenticated, IsAdminUser, AsyncBasePermission
    
    class HasPremiumSubscriptionAsync(AsyncBasePermission):
        async def has_permission_async(self, request, controller):
            user_profile = await request.user.profile.aget()
            return user_profile.has_premium_subscription
    
    @api_controller("/content")
    class ContentController:
        # User must be authenticated AND have premium subscription
        @http_get("/premium", permissions=[IsAuthenticated() & HasPremiumSubscriptionAsync()])
        async def premium_content(self, request):
            return {"content": "Premium content"}
        
        # User must be authenticated OR an admin
        @http_get("/special", permissions=[IsAuthenticated() | IsAdminUser()])
        async def special_content(self, request):
            return {"content": "Special content"}
        
        # User must be authenticated but NOT an admin
        @http_get("/regular", permissions=[IsAuthenticated() & ~IsAdminUser()])
        async def regular_content(self, request):
            return {"content": "Regular user content"}
  8. How Model Controllers generate schemas

    master

    When ninja-schema is installed, Model Controllers automatically generate Pydantic schemas for input validation, output serialization, and OpenAPI documentation.

    Typically, two types of schemas are generated:

    1. Create/Update Schemas: Used for POST and PUT/PATCH requests (e.g., EventCreateSchema).
    2. Retrieve Schemas: Used for GET requests to represent the model data (e.g., EventSchema), which includes the model's id.
  9. How to call routes in TestClient with static prefixes

    master

    When using TestClient or TestAsyncClient with a controller that has a static prefix (defined in @api_controller), you should call the endpoints using the path defined on the specific route method, omitting the controller's static prefix. The testing client automatically wires the controller and resolves the routes internally.

    @api_controller('/api', tags=['Users'])
    class UserController:
        @route.get('/users')
        def list_users(self):
            return []
    
    def test_get_users():
        client = TestClient(UserController)
        # Use '/users', NOT '/api/users'
        response = client.get('/users')
        assert response.status_code == 200
  10. How permissions work in controllers

    master

    Django Ninja Extra provides an advanced permission system. You can apply permissions at the controller level (affecting all routes within the class) or override them at the individual route level.

    To create a custom permission, inherit from PermissionBase and implement the has_permission(self, context) method, where context.request provides access to the Django request object.

    from ninja_extra import api_controller, http_get
    from ninja_extra.permissions import IsAuthenticated, PermissionBase
    
    # Custom permission implementation
    class IsAdmin(PermissionBase):
        def has_permission(self, context):
            return context.request.user.is_staff
    
    # Controller-level permissions
    @api_controller('/admin', tags=['Admin'], permissions=[IsAuthenticated, IsAdmin])
    class AdminController:
        @http_get('/stats')
        def get_stats(self):
            return {"status": "admin only data"}
        
        # Route-level permission override (making it public)
        @http_get('/public', permissions=[])
        def public_stats(self):
            return {"status": "public data"}
  11. Combine permissions using logical operators

    master

    You can combine multiple permission requirements using logical operators:

    • & (AND): Both permissions must pass.
    • | (OR): At least one permission must pass.
    • ~ (NOT): Inverts the permission result.
    from ninja_extra import permissions, api_controller, http_get
    
    @api_controller("/content")
    class ContentController:
        # Requires authentication OR a premium subscription
        @http_get("/basic", permissions=[permissions.IsAuthenticated() | HasPremiumSubscription()])
        def basic_content(self):
            return {"content": "Basic content"}
        
        # Requires authentication AND a premium subscription
        @http_get("/premium", permissions=[permissions.IsAuthenticated() & HasPremiumSubscription()])
        def premium_content(self):
            return {"content": "Premium content"}
        
        # Requires authentication AND NOT a premium subscription
        @http_get("/non-premium", permissions=[permissions.IsAuthenticated() & ~HasPremiumSubscription()])
        def non_premium_content(self):
            return {"content": "Content for non-premium users"}