简介 Django Ninja 是一个 Web 框架,用于使用 Django 和 Python 3.6+ 类型提示构建 API。
主要特征:
简单: 设计易于使用且直观。快速执行: 由于Pydantic和异步支持,性能非常高。快速编码: 类型提示和自动文档让您只关注业务逻辑。基于标准: 基于API的开放标准:OpenAPI(以前称为Swagger)和JSON Schema。Django 友好: (显然)与 Django 核心和 ORM 具有良好的集成。交互式 API 文档: (由OpenAPI / Swagger UI或Redoc提供)启动项目 安装 1 pip install django-ninja
创建项目 1 django-admin startproject apidemo
基础配置 在urls.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from django.contrib import adminfrom django.urls import pathfrom ninja import NinjaAPIapi = NinjaAPI() @api.get("/add" ) def add (request, a: int , b: int ): return {"result" : a + b} urlpatterns = [ path("admin/" , admin.site.urls), path("api/" , api.urls), ]
启动项目 1 python manage.py runserver
打开交互式文档 浏览器打开: http://127.0.0.1:8000/api/docs
将看到自动的交互式 API 文档(由OpenAPI / Swagger UI或Redoc提供):
请求数据 路径参数 路径参数 item_id 的值将作为参数 item_id 传递给您的函数。
1 2 3 4 5 6 7 8 9 10 11 @api.get("/items/{item_id}" ) def read_item (request, item_id ): return {"item_id" : item_id} @api.get("/items/{item_id}" ) def read_item (request, item_id: int ): return {"item_id" : item_id}
item_id会根据在接口输入的值返回:
也可进行路径参数转换:
1 2 3 4 @api.get("/items/{int:item_id}" ) def read_item (request, item_id ): return {"item_id" : item_id}
查询参数 分页查询 1 2 3 4 5 6 weapons = ["Ninjato" , "Shuriken" , "Katana" , "Kama" , "Kunai" , "Naginata" , "Yari" ] @api.get("/weapons" ) def list_weapons (request, limit: int = 10 , offset: int = 0 ): return weapons[offset: offset + limit]
字符串匹配 1 2 3 4 5 @api.get("/weapons/search" ) def search_weapons (request, q: str , offset: int = 0 ): results = [w for w in weapons if q in w.lower()] print (q, results) return results[offset: offset + 10 ]
使用Schema 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from typing import List from pydantic import Fieldfrom ninja import Query, Schemaclass Filters (Schema ): limit: int = 100 offset: int = None query: str = None category__in: List [str ] = Field(None , alias="categories" ) @api.get("/filter" ) def events (request, filters: Filters = Query(... ) ): return {"filters" : filters.dict ()}
Query()函数 是用来标记filters是查询参数
请求体 使用类模型schema的方式: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from ninja import Routerfrom ninja import Schemaapi = Router() class Item (Schema ): name: str description: str = None price: float quantity: int @api.post("/items" ) def create (request, item: Item ): return item
=None作为默认值,说明这个字段可不传值为空
请求体+路径参数 1 2 3 4 5 @api.put("/items/{item_id}" ) def update (request, item_id: int , item: Item ): return {"item_id" : item_id, "item" : item.dict ()}
请求体+路径参数+查询参数 1 2 3 4 5 @api.post("/items/{item_id}" ) def update (request, item_id: int , item: Item, q: str ): return {"item_id" : item_id, "item" : item.dict (), "q" : q}
三者同时包含解释:
如果该参数也在路径中声明,它将被用作路径参数 如果参数是单数类型(如 int、float、str、bool 等),它将被解释为查询参数 如果参数声明为 Schema(或 Pydantic BaseModel)类型,它将被解释为请求体。 1 2 3 @api.post("/login" ) def login (request, username: str = Form(... ), password: str = Form(... ) ): return {'username' : username, 'password' : '*****' }
使用Schema 1 2 3 4 5 6 7 8 9 10 class Item (Schema ): name: str description: str = None price: float quantity: int @api.post("/items" ) def create (request, item: Item = Form(... ) ): return item
表单+路径参数+查询参数 1 2 3 @api.post("/items/{item_id}" ) def update (request, item_id: int , q: str , item: Item = Form(... ) ): return {"item_id" : item_id, "item" : item.dict (), "q" : q}
可选表单空值设置 可选的表单字段通常以空值发送。该值被解释为空字符串,因此可能无法验证 int 或 bool 等字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from ninja import Form, Schemafrom pydantic.fields import ModelFieldfrom typing import Generic , TypeVarPydanticField = TypeVar("PydanticField" ) class EmptyStrToDefault (Generic [PydanticField]): @classmethod def __get_validators__ (cls ): yield cls.validate @classmethod def validate (cls, value: PydanticField, field: ModelField ) -> PydanticField: if value == "" : return field.default return value class Item (Schema ): name: str description: str = None price: EmptyStrToDefault[float ] = 0.0 quantity: EmptyStrToDefault[int ] = 0 in_stock: EmptyStrToDefault[bool ] = True @api.post("/items-blank-default" ) def update (request, item: Item=Form(... ) ): return item.dict ()
文件上传 单文件上传 1 2 3 4 5 6 7 8 from ninja import NinjaAPI, Filefrom ninja.files import UploadedFile@api.post("/upload" ) def upload (request, file: UploadedFile = File(... ) ): data = file.read() return {'name' : file.name, 'len' : len (data)}
多文件上传 1 2 3 4 5 6 7 8 from typing import List from ninja import NinjaAPI, Filefrom ninja.files import UploadedFile@api.post("/upload-many" ) def upload_many (request, files: List [UploadedFile] = File(... ) ): return [f.name for f in files]
上传带有额外字段的文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from ninja import NinjaAPI, Schema, UploadedFile, Form, Filefrom datetime import dateapi = NinjaAPI() class UserDetails (Schema ): first_name: str last_name: str birthdate: date @api.post('/user' ) def create_user (request, details: UserDetails = Form(... ), file: UploadedFile = File(... ) ): return [details.dict (), file.name]
UploadedFile是Django的UploadFile的一个别名,它拥有访问上传文件的所有方法和属性
read() multiple_chunks(chunk_size=None)如果上传的文件足够大,需要分块读取,返回True。默认情况下是大于2.5M的文件。 chunks(chunk_size=None) name size content_type etc. 请求解析 YAML 解析器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import yamlfrom typing import List from ninja import NinjaAPI, Schemafrom ninja.parser import Parserclass MyYamlParser (Parser ): def parse_body (self, request ): return yaml.safe_load(request.body) api = NinjaAPI(parser=MyYamlParser()) class Payload (Schema ): ints: List [int ] string: str f: float @api.post('/yaml' ) def operation (request, payload: Payload ): return payload.dict ()
如果像这样发送 YAML 作为请求正文:
1 2 3 4 5 6 ints: - 0 - 1 string: hello f: 3.14
它将被正确解析,并且有如下 JSON 输出:
1 2 3 4 5 6 7 8 9 { "ints" : [ 0 , 1 ] , "string" : "hello" , "f" : 3.14 }
ORJSON 解析器 orjson 是一个快速、准确的 Python JSON 库。它被评为最快的 JSON Python 库,并且比标准 json 库或其他第三方库更准确。
1 2 # 安装库 pip install orjson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import orjsonfrom ninja import NinjaAPI, Schemafrom ninja.parser import Parserclass ORJSONParser (Parser ): def parse_body (self, request ): return orjson.loads(request.body) api = NinjaAPI(parser=ORJSONParser()) class Payload (Schema ): ints: str string: str f: float @api.post('/json' ) def operation (request, payload: Payload ): return payload.dict ()
过滤filtering 如果想通过许多不同的属性来过滤查询集,则将过滤器封装到 FilterSchema 类中是有意义的。 FilterSchema 是一个常规 Schema,因此它使用了 Pydantic 的所有必要功能,但它还添加了一些花哨的功能,可以轻松地将面向用户的过滤参数转换为数据库查询。
FilterSchema 子类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from ninja import FilterSchema, Field, Queryfrom typing import Optional from django.core import serializersclass BookFilterSchema (FilterSchema ): first_name: Optional [str ] last_name: Optional [str ] @router.get("/books" ) def list_books (request, filters: BookFilterSchema = Query(... ) ): books = Employee.objects.all () book = filters.filter (books) seria_books = serializers.serialize("json" , book) return seria_books
Q表达式 1 2 3 4 5 6 7 8 9 @router.get("/q/books" ) def list_books (request, filters: BookFilterSchema = Query(... ) ): q = Q(first_name__is_active=True ) | Q(last_name__is_active=True ) q &= filters.get_filter_expression() return Employee.objects.filter (q)
自定义字段 当数据库查询比这更复杂时,你可以在字段定义中使用 “q “kwarg明确指定它们
1 2 3 4 class BookFilterSchema (FilterSchema ): search: Optional [str ] = Field(q=['name__icontains' , 'author__name__icontains' , 'publisher__name__icontains' ])
组合表达式 字段级表达式使用 OR 运算符连接在一起。 字段本身使用 AND 运算符连接在一起。 可以在字段级和类级定义中使用 expression_connector 参数自定义此行为:
1 2 3 4 5 6 7 8 class BookFilterSchema (FilterSchema ): active: Optional [bool ] = Field(q=['is_active' , 'publisher__is_active' ], expression_connector='AND' ) name: Optional [str ] = Field(q='name__icontains' ) class Config : expression_connector = 'OR'
表达式连接器可以采用“OR”、“AND”和“XOR”的值,但后者仅在 Django 4.1 中受支持。
无过滤 可以使 FilterSchema 将 None 视为应进行过滤的有效值。 这可以使用ignore_none kwarg在字段级别完成:
1 2 3 4 class BookFilterSchema (FilterSchema ): name: Optional [str ] = Field(q='name__icontains' ) tag: Optional [str ] = Field(q='tag' , ignore_none=False )
自定义表达式 有时可能想拥有复杂的过滤场景,而这些场景不能由单个字段注释来处理。对于这种情况,你可以把你的字段过滤逻辑作为一个自定义方法来实现。简单地定义一个名为filter_的方法,它接收一个过滤值并返回一个Q表达式:
1 2 3 4 5 6 class BookFilterSchema (FilterSchema ): tag: Optional [str ] popular: Optional [bool ] def filter_popular (self, value: bool ) -> Q: return Q(view_count__gt=1000 ) | Q(download_count__gt=100 ) if value else Q()
这样的字段方法优先于相应字段的Field()
定义中的内容。 如果这还不够,可以在一个custom_expression方法中为整个FilterSet类实现你自己的自定义过滤逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class BookFilterSchema (FilterSchema ): first_name: Optional [str ] last_name: Optional [str ] def custom_expression (self ) -> Q: q = Q() if self .first_name: q &= Q(first_name__icontains=self .first_name) if self .last_name: q &= ( Q(department_id__gt=10 ) | Q(cv='003644wOJAG' ) ) return q
custom_expression方法优先于前面描述的任何其他定义,包括filter_方法。
处理响应 Response Schema models.py
中添加模型表字段
1 2 3 4 from django.contrib.auth.models import AbstractBaseUserclass User (AbstractBaseUser ): username = models.CharField(max_length=50 ) password = models.CharField(max_length=30 )
settings.py
配置:
1 2 3 PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.MD5PasswordHasher' , )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from ninja import FilterSchema, Field, Router, Queryfrom ninja import Schemafrom system.models import *api = Router() class UserIn (Schema ): username: str password: str class UserOut (Schema ): id : int username: str @api.post("/users/" , response=UserOut ) def create_user (request, data: UserIn ): print (data.username) print (data.password) user = User(username=data.username) user.set_password(data.password) user.save()
嵌套对象 - Nested objects 假设我们有一个带有用户外键的任务 Django 模型:
1 2 3 4 class Task (models.Model): title = models.CharField(max_length=200 ) is_completed = models.BooleanField(default=False ) owner = models.ForeignKey("auth.User" , null=True , blank=True , on_delete=models.CASCADE)
通常还需要返回一些嵌套/子对象的响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from typing import List from ninja import Schemaclass UserSchema (Schema ): id : int first_name: str last_name: str class TaskSchema (Schema ): id : int title: str is_completed: bool owner: UserSchema = None @api.get("/tasks" , response=List [TaskSchema] ) def tasks (request ): queryset = Task.objects.select_related("owner" ) return list (queryset)
别名 - Aliases Ninja Schema 对象扩展了 Pydantic 的 Field(…, alias=””) 格式以处理点响应。
1 2 3 4 5 6 7 8 9 10 from ninja import Field, Schemaclass TaskSchema (Schema ): id : int title: str completed: bool = Field(..., alias="is_completed" ) owner_first_name: str = Field(None , alias="owner.first_name" )
别名还支持 django 模板语法变量访问:
1 2 3 class TaskSchema (Schema ): last_message: str = Field(None , alias="message_set.0.text" )
解析器 - Resolvers 也可以通过基于字段名的解析方法创建计算字段。 该方法必须接受一个单一的参数,这将是模式所要解析的对象。 当作为一个标准方法创建解析器时,self使你能够访问模式中其他经过验证和格式化的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class TaskSchema (Schema ): id : int title: str is_completed: bool owner: Optional [str ] lower_title: str email: str @staticmethod def resolve_owner (obj ): if not obj.owner: return return f"{obj.owner.first_name} {obj.owner.last_name} " def resolve_lower_title (self, obj ): return self .title.lower() def resolve_email (self, obj ): return f"{obj.owner.email} "
返回查询集 1 2 3 @api.get("/tasks/all" , response=List [TaskSchema] ) def tasks (request ): return Task.objects.all ()
如果您的操作是异步的,则此示例将不起作用,因为需要安全地调用 ORM 查询。
文件字段和图像字段 Django Ninja默认将文件和图片(用FileField或ImageField声明)转换为字符串URL。models.py
创建模型表:
1 2 3 class Picture (models.Model): title = models.CharField(max_length=100 ) image = models.ImageField(upload_to='images' )
**api
**示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from ninja import Schema, Filefrom ninja.files import UploadedFilefrom system.models import *class PictureSchema (Schema ): title: str class PictureRsp (Schema ): title: str image: str @api.post("/tasks/upload" , response=PictureRsp ) def tasks (request, item: PictureSchema, img: UploadedFile = File(... ) ): payload_dict = item.dict () picture = Picture(**payload_dict) picture.image.save(img.name, img) return picture
多重响应模式 有时你需要定义多个响应模式。例如,在认证的情况下,你可以返回:
200 successful -> token 401 -> Unauthorized 402 -> Payment required etc.. 可以向响应参数传递一个字典,其中: key 是响应代码 value 是该代码的模式 另外,当你返回结果时,你还必须传递一个状态码来告诉Django Ninja应该使用哪种模式来进行验证和序列化。如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Token (Schema ): token: str expires: date class Message (Schema ): message: str @api.post('/login' , response={200 : Token, 401 : Message, 402 : Message} ) def login (request, payload: Auth ): if auth_not_valid: return 401 , {'message' : 'Unauthorized' } if negative_balance: return 402 , {'message' : 'Insufficient balance amount. Please proceed to a payment page.' } return 200 , {'token' : xxx, ...}
多个响应代码 为了避免消息模式重复,可以为一个模式使用多个响应代码:
1 2 3 4 5 6 7 8 9 10 11 ... from ninja.responses import codes_4xx@api.post('/login' , response={200 : Token, codes_4xx: Message} ) def login (request, payload: Auth ): if auth_not_valid: return 401 , {'message' : 'Unauthorized' } if negative_balance: return 402 , {'message' : 'Insufficient balance amount. Please proceed to a payment page.' } return 200 , {'token' : xxx, ...}
Django Ninja自带以下HTTP代码:
1 2 3 4 5 from ninja.responses import codes_1xxfrom ninja.responses import codes_2xxfrom ninja.responses import codes_3xxfrom ninja.responses import codes_4xxfrom ninja.responses import codes_5xx
也可以使用frozenset创建你自己的范围:
1 2 3 my_codes = frozenset ({416 , 418 , 425 , 429 , 451 }) @api.post('/login' , response={200 : Token, my_codes: Message} )
空响应 有的响应,例如204无内容,没有主体。为了表示响应的主体是空的,可以用None而不是Schema来标记响应参数:
1 2 3 @api.post("/no_content" , response={204 : None } ) def no_content (request ): return 204 , None
自引用方案 有时需要创建一个引用自身或树结构对象的模式。为此,需要:
在引号中设置模式类型 使用 update_forward_refs
方法应用自引用类型1 2 3 4 5 6 7 8 9 10 11 class Organization (Schema ): title: str part_of: 'Organization' = None Organization.update_forward_refs() @api.get('/organizations' , response=List [Organization] ) def list_organizations (request ): ...
create_schema()自引用方案 为了能够从通过 create_schema() 生成的模式中使用 update_forward_refs() 方法,类的“名称”需要位于我们的命名空间中。在这种情况下,将 name 参数传递给 create_schema() 非常重要
1 2 3 4 5 6 7 8 9 UserSchema = create_schema( User, name='UserSchema' , fields=['id' , 'username' ] custom_fields=[ ('manager' , 'UserSchema' , None ), ] ) UserSchema.update_forward_refs()
在视图之外序列化 你的对象的序列化可以通过使用模式对象上的.from_orm()方法直接在代码中完成。创建如下模型:
1 2 class Person (models.Model): name = models.CharField(max_length=50 )
如下schema:
1 2 class PersonSchema (Schema ): name: str
可以使用模式上的 .from_orm() 方法执行直接序列化。拥有模式对象的实例后,.dict() 和 .json() 方法允许您获取字典输出和字符串 JSON 版本。
1 2 3 4 5 6 7 8 >>> person = Person.objects.get(id =1 )>>> data = PersonSchema.from_orm(person)>>> dataPersonSchema(id =1 , name='Mr. Smith' ) >>> data.dict (){'id' :1 , 'name' :'Mr. Smith' } >>> data.json()'{"id":1, "name":"Mr. Smith"}'
多个项目:或查询集(或列表):
1 2 3 >>> persons = Person.objects.all ()>>> data = [PersonSchema.from_orm(i).dict () for i in persons][{'id' :1 , 'name' :'Mr. Smith' },{'id' : 2 , 'name' : 'Mrs. Smith' }...]
Django HTTP responses 也可以返回常规的django http响应:
1 2 3 4 5 6 7 8 9 10 11 12 from django.http import HttpResponsefrom django.shortcuts import redirect@api.get("/http" ) def result_django (request ): return HttpResponse('some data' ) @api.get("/something" ) def some_redirect (request ): return redirect("/some-path" )
改变响应 有时您会想在响应提供之前更改响应,例如添加标头或更改 cookie。 为此,只需声明一个 HttpResponse 类型的函数参数:
1 2 3 4 5 6 7 8 9 from django.http import HttpRequest, HttpResponse@api.get("/cookie/" ) def feed_cookiemonster (request: HttpRequest, response: HttpResponse ): response.set_cookie("cookie" , "delicious" ) response["X-Cookiemonster" ] = "blue" return {"cookiemonster_happy" : True }
更改基本响应对象 可以通过重写 NinjaAPI.create_temporal_response
方法来更改此时间响应对象。
1 2 3 4 def create_temporal_response (self, request: HttpRequest ) -> HttpResponse: response = super ().create_temporal_response(request) return response
Django模型的模式 模式对于定义验证规则和响应非常有用,但有时您需要将数据库模型反映到模式中并保持更改同步。
ModelSchema ModelSchema 是一个特殊的基类,可以从模型自动生成模式。 所需要做的就是在模式配置上设置 model 和 model_fields 属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from django.contrib.auth.models import Userfrom ninja import ModelSchemaclass UserSchema (ModelSchema ): class Config : model = User model_fields = ['id' , 'username' , 'first_name' , 'last_name' ]
使用所有模型字段 要使用模型中的所有字段 - 可以将 __all__
传递给 model_fields
:
1 2 3 4 class UserSchema (ModelSchema ): class Config : model = User model_fields = "__all__"
不建议全部使用。 这可能会导致意外的不需要的数据泄露(例如上例中的散列密码)。 一般建议 - 使用 model_fields 显式定义您希望在 API 中可见的字段列表。
排除模型字段 要使用除少数字段之外的所有字段,您可以使用 model_exclude
配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class UserSchema (ModelSchema ): class Config : model = User model_exclude = ['password' , 'last_login' , 'user_permissions' ]
覆盖字段 要更改某些字段的默认注释或添加新字段,只需照常使用带注释的属性即可。
1 2 3 4 5 6 7 8 9 10 11 12 class GroupSchema (ModelSchema ): class Config : model = Group model_fields = ['id' , 'name' ] class UserSchema (ModelSchema ): groups: List [GroupSchema] = [] class Config : model = User model_fields = ['id' , 'username' , 'first_name' , 'last_name' ]
字段可选 对于 PATCH API 操作,您通常需要将模式的所有字段设为可选。为此,可以使用配置 model_fields_optional
:
1 2 3 4 5 class PatchGroupSchema (ModelSchema ): class Config : model = Group model_fields = ['id' , 'name' , 'description' ] model_fields_optional = '__all__'
也可以定义多个可选的字段而不是全部:
1 model_fields_optional = ['name' , 'description' ]
使用create_schema 在底层,ModelSchema 使用 create_schema
函数。这是一种更高级(且不太安全)的方法 - 请谨慎使用。 Django Ninja 带有一个辅助函数 create_schema:
1 2 3 4 5 6 7 8 9 def create_schema ( model, name = "" , depth = 0 , fields: list [str ] = None , exclude: list [str ] = None , optional_fields: list [str ] | str = None , custom_fields: list [tuple (str , Any , Any )] = None , )
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from django.contrib.auth.models import Userfrom ninja.orm import create_schemaUserSchema = create_schema(User)
默认情况下,create_schema 会构建包含所有模型字段的模式。这可能会导致意外的不需要的数据泄露(例如上例中的散列密码)。 始终使用字段或排除参数来显式定义属性列表。
使用字段 - fields 1 2 3 4 5 6 7 UserSchema = create_schema(User, fields=['id' , 'username' ])
使用排除 - exclude 1 2 3 4 5 6 7 8 9 10 11 12 13 14 UserSchema = create_schema(User, exclude=[ 'password' , 'last_login' , 'is_superuser' , 'is_staff' , 'groups' , 'user_permissions' ] )
使用深度 - depth The depth argument allows you to introspect the Django model into the Related fields(ForeignKey, OneToOne, ManyToMany).
1 2 3 4 5 6 7 UserSchema = create_schema(User, depth=1 , fields=['username' , 'groups' ])
此处的组变成了 List[Group]
- Many2many 字段内省了 1 级并为组创建了模式:
1 2 3 4 class Group (Schema ): id : int name: str permissions: List [int ]
重写 Pydantic 配置 通过架构的 Pydantic Config 类,可以对 Django Ninja 架构进行许多自定义。
Django Ninja 使用 Pydantic 模型及其所有功能和优点。选择别名 Schema 是为了避免使用 Django 模型时代码混乱,因为 Pydantic 的模型类默认称为 Model,并且与 Django 的 Model 类冲突。
驼峰模式 一个有趣的 Config 属性是 alias_generator
。在 Django Ninja 中使用 Pydantic 的示例可能类似于:
1 2 3 4 5 6 7 8 9 10 11 12 13 from ninja import Schemadef to_camel (string: str ) -> str : return '' .join(word.capitalize() for word in string.split('_' )) class CamelModelSchema (Schema ): str_field_name: str float_field_name: float class Config (Schema.Config): alias_generator = to_camel
当重写模式的 Config 时,需要从 Config 基类继承。
想要修改字段名称的输出(例如驼峰命名法)时 - 还需要设置allow_population_by_field_name和by_alias
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from ninja import ModelSchemadef to_camel (string: str ) -> str : return '' .join(word.capitalize() for word in string.split('_' )) class UserSchemas (ModelSchema ): class Config : model = User model_fields = ["username" ] alias_generator = to_camel allow_population_by_field_name = True @api.get("/users" , response=list [UserSchemas], by_alias=True ) def get_users (request ): return User.objects.all ()
Django 模型的自定义配置 使用 create_schema
时,生成的模式可用于构建另一个具有自定义配置的类,例如:
1 2 3 4 5 6 7 8 9 10 11 from django.contrib.auth.models import Userfrom ninja.orm import create_schemaBaseUserSchema = create_schema(User) class UserSchema (BaseUserSchema ): class Config (BaseUserSchema.Config): ...
Django Ninja 附带分页支持。这允许您将大型结果集拆分为单独的页面。
要将分页应用于函数 - 只需应用分页装饰器:
1 2 3 4 5 6 from ninja.pagination import paginate@api.get('/users' , response=List [UserSchema] ) @paginate def list_users (request ): return User.objects.all ()
可以使用 limit 和 offset GET 参数查询用户
/api/users?limit=10&offset=0
默认限制设置为 100 (您可以使用 NINJA_PAGINATION_PER_PAGE 在 settings.py 中更改它)
内置分页类 LimitOffsetPagination (default) 这是默认的分页类(您可以使用 NINJA_PAGINATION_CLASS 类路径在 settings.py 中更改它)
1 2 3 4 5 6 from ninja.pagination import paginate, LimitOffsetPagination@api.get('/users' , response=List [UserSchema] ) @paginate(LimitOffsetPagination ) def list_users (request ): return User.objects.all ()
该类有两个输入参数:
limit - 定义页面上的查询集数量(默认 = 100,在 NINJA_PAGINATION_PER_PAGE 中更改) offset - 设置页面窗口偏移量(默认值:0,索引从 0 开始) PageNumberPagination 该类有一个参数页,页码从 1 开始,默认每页输出 100 个查询集(可以使用 settings.py 进行更改)
1 2 3 4 5 6 from ninja.pagination import paginate, PageNumberPagination@api.get('/users' , response=List [UserSchemas] ) @paginate(PageNumberPagination ) def list_users (request ): return User.objects.all ()
还可以为每个视图单独设置自定义 page_size 值:
1 2 3 @api.get("/users" ) @paginate(PageNumberPagination, page_size=50 ) def list_users (...
视图函数中访问分页器参数 如果您需要访问视图函数中用于分页的输入参数 - 使用 pass_parameter 参数 在这种情况下,输入数据将在 **kwargs 中可用:
1 2 3 4 5 @api.get("/someview" ) @paginate(pass_parameter="pagination_info" ) def someview (request, **kwargs ): page = kwargs["pagination_info" ].page return ...
创建自定义分页类 要创建自定义分页类,您应该子类化 ninja.pagination.PaginationBase 并覆盖输入和输出模式类以及 paginate_queryset(self, queryset, request, **params) 方法:
输入模式是一个模式类,它描述应该传递给分页器的参数(例如页码或限制/偏移值)。 输出模式描述页面输出的模式(例如计数/下一页/项目/等)。 paginate_queryset 方法会传递初始查询集,并应返回一个仅包含所请求页面中的数据的可迭代对象。该方法接受以下参数:queryset:api 函数返回的查询集(或可迭代) pagination - 分页器。输入参数(已解析和验证) **params:kwargs 将包含装饰函数收到的所有参数 Example:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from ninja.pagination import paginate, PaginationBasefrom ninja import Schemafrom typing import List , Any class CustomPagination (PaginationBase ): class Input (Schema ): skip: int class Output (Schema ): items: List [Any ] total: int per_page: int def paginate_queryset (self, queryset, pagination: Input, **params ): skip = pagination.skip return { 'items' : queryset[skip : skip + 5 ], 'total' : queryset.count(), 'per_page' : 5 , } @api.get('/users' , response=List [UserSchemas] ) @paginate(CustomPagination ) def list_users (request ): return User.objects.all ()
输出属性 - Output attribute 默认情况下,页面项目放置在“items”属性中。要覆盖此行为,请使用 items_attribute:
1 2 3 4 5 6 7 8 class CustomPagination (PaginationBase ): ... class Output (Schema ): results: List [Any ] total: int per_page: int items_attribute: str = "results"
一次将分页应用于多个操作 通常情况下,需要向返回查询集或列表的所有视图添加分页
可以使用内置路由器类(RouterPaginated)自动将分页注入到定义response=List[SomeSchema]的所有操作中:
1 2 3 4 5 6 7 8 9 10 11 12 from ninja.pagination import RouterPaginatedfrom typing import List router = RouterPaginated() class UserSchemas (Schema ): username: str @api.get('/routers/users' , response=List [UserSchemas] ) def list_users (request ): return User.objects.all ()
要将分页应用于主 api 实例,请使用 default_router 参数:
1 2 3 api = NinjaAPI(default_router=RouterPaginated()) @api.get(...
响应渲染器 - Response renderers REST API 最常见的响应类型通常是 JSON。 Django Ninja 还支持定义您自己的自定义渲染器,这使您可以灵活地设计自己的媒体类型。
创建渲染器 要创建自己的渲染器,需要继承ninja.renderers.BaseRenderer并重写render方法。然后,您可以将类的实例作为渲染器参数传递给 NinjaAPI:
渲染方法采用以下参数:
request -> HttpRequest 对象 data -> 需要序列化的对象 response_status as an int -> 将返回给客户端的 HTTP 状态代码 还需要在类上定义 media_type 属性来设置响应的内容类型标头。
ORJSON 渲染器 orjson 是一个快速、准确的 Python JSON 库。它被评为最快的 JSON Python 库,并且比标准 json 库或其他第三方库更准确。它还本机序列化数据类、日期时间、numpy 和 UUID 实例。
下面是一个使用 orjson 的示例渲染器类:
1 2 3 4 5 6 7 8 9 10 11 12 import orjsonfrom ninja import NinjaAPIfrom ninja.renderers import BaseRendererclass ORJSONRenderer (BaseRenderer ): media_type = "application/json" def render (self, request, data, *, response_status ): return orjson.dumps(data) api = NinjaAPI(renderer=ORJSONRenderer())
XML 渲染器 这是创建将所有响应输出为 XML 的渲染器的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from io import StringIOfrom django.utils.encoding import force_strfrom django.utils.xmlutils import SimplerXMLGeneratorfrom ninja import NinjaAPIfrom ninja.renderers import BaseRendererclass XMLRenderer (BaseRenderer ): media_type = "text/xml" def render (self, request, data, *, response_status ): stream = StringIO() xml = SimplerXMLGenerator(stream, "utf-8" ) xml.startDocument() xml.startElement("data" , {}) self ._to_xml(xml, data) xml.endElement("data" ) xml.endDocument() return stream.getvalue() def _to_xml (self, xml, data ): if isinstance (data, (list , tuple )): for item in data: xml.startElement("item" , {}) self ._to_xml(xml, item) xml.endElement("item" ) elif isinstance (data, dict ): for key, value in data.items(): xml.startElement(key, {}) self ._to_xml(xml, value) xml.endElement(key) elif data is None : pass else : xml.characters(force_str(data)) api = NinjaAPI(renderer=XMLRenderer())
路由器 多应用路由 当有多个应用时,在每个应用中的创建一个api.py模块(或者直接在views.py模块)中写各自的路由。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from datetime import datefrom ninja import Router, Schemafrom system.models import *router = Router() class EmployeeIn (Schema ): first_name: str last_name: str department_id: int = None birthdate: date = None @router.post("/employees" ) def create_employee (request, payload: EmployeeIn ): employee = Employee.objects.create(**payload.dict ()) return {"id" : employee.id }
路由器认证 使用 auth 参数将验证器应用于路由器声明的所有操作:
1 api.add_router("/events/" , events_router, auth=BasicAuth())
或使用路由器构造函数:
1 router = Router(auth=BasicAuth())
路由器标签 可以使用标签参数将标签应用于路由器声明的所有操作:
1 api.add_router("/events/" , events_router, tags=["events" ])
或者使用路由器构造函数:
1 router = Router(tags=["events" ])
嵌套路由器 有时您需要进一步拆分逻辑。 Django Ninja 可以将一个路由器多次包含到另一个路由器中,最后将顶级路由器包含到主 api 实例中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from django.contrib import adminfrom django.urls import pathfrom ninja import NinjaAPI, Routerapi = NinjaAPI() first_router = Router() second_router = Router() third_router = Router() @api.get("/add" ) def add (request, a: int , b: int ): return {"result" : a + b} @first_router.get("/add" ) def add (request, a: int , b: int ): return {"result" : a + b} @second_router.get("/add" ) def add (request, a: int , b: int ): return {"result" : a + b} @third_router.get("/add" ) def add (request, a: int , b: int ): return {"result" : a + b} second_router.add_router("l3" , third_router) first_router.add_router("l2" , second_router) api.add_router("l1" , first_router) urlpatterns = [ path("admin/" , admin.site.urls), path("api/" , api.urls), ]
然后就有如下路由路径:
1 2 3 4 /api/add /api/l1/add /api/l1/l2/add /api/l1/l2/l3/add
Authentication Django Ninja 提供了多种工具来帮助您以标准方式轻松、快速地处理身份验证和授权,而无需学习所有安全规范。 核心概念是,当你描述一个API操作时,你可以定义一个认证对象。
1 2 3 4 5 6 7 8 9 from ninja import NinjaAPIfrom ninja.security import django_authapi = NinjaAPI(csrf=True ) @api.get("/pets" , auth=django_auth ) def pets (request ): return f"Authenticated user {request.auth} "
在此示例中,客户端只有使用 Django 会话身份验证(默认基于 cookie)才能调用 pets 方法,否则将返回 HTTP-401 错误。
自动OpenAPI模式 如下示例,客户端为了进行身份验证,需要传递标头:
1 2 3 4 5 6 7 8 9 10 11 12 from ninja.security import HttpBearerclass AuthBearer (HttpBearer ): def authenticate (self, request, token ): if token == "supersecret" : return token @api.get("/bearer" , auth=AuthBearer( ) ) def bearer (request ): return {"token" : request.auth}
全局认证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from ninja import NinjaAPI, Formfrom ninja.security import HttpBearerclass GlobalAuth (HttpBearer ): def authenticate (self, request, token ): if token == "supersecret" : return token api = NinjaAPI(auth=GlobalAuth()) @api.post("/token" , auth=None ) def get_token (request, username: str = Form(... ), password: str = Form(... ) ): if username == "admin" and password == "giraffethinnknslong" : return {"token" : "supersecret" }
可用的身份验证选项 自定义功能 “auth=”参数接受任何 Callable 对象。仅当可调用对象返回可转换为布尔值 True 的值时,NinjaAPI 才会通过身份验证。该返回值将分配给 request.auth 属性。
1 2 3 4 5 6 7 8 def ip_whitelist (request ): if request.META["REMOTE_ADDR" ] == "8.8.8.8" : return "8.8.8.8" @api.get("/ipwhiltelist" , auth=ip_whitelist ) def ipwhiltelist (request ): return f"Authenticated client, IP = {request.auth} "
api key 某些 API 使用 API 密钥进行授权。 API 密钥是客户端在进行 API 调用时提供的用于识别自身身份的令牌。密钥可以在查询字符串中发送:
GET /something?api_key=abcdef12345
或作为一个请求头:
GET /something HTTP/1.1 X-API-Key: abcdef12345
或作为 cookie:
GET /something HTTP/1.1 Cookie: X-API-KEY=abcdef12345
Django Ninja 附带了内置类来帮助处理这些情况。
in Query 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Client (models.Model): key = models.CharField(max_length=100 ) from ninja.security import APIKeyQueryfrom system.models import Clientclass ApiKey (APIKeyQuery ): param_name = "api_key" def authenticate (self, request, key ): try : return Client.objects.get(key=key) except Client.DoesNotExist: pass api_key = ApiKey() @router.get("/apikey" , auth=api_key ) def apikey (request ): assert isinstance (request.auth, Client) return f"Hello {request.auth} "
在此示例中,我们从 GET[‘api_key’] 获取一个令牌,并在数据库中找到与该密钥对应的客户端。 Client 实例将被设置为 request.auth 属性。
param_name 是要检查的 GET 参数的名称。如果未设置,将使用默认的“key”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from ninja.security import APIKeyHeaderclass ApiKey (APIKeyHeader ): param_name = "X-API-Key" def authenticate (self, request, key ): if key == "supersecret" : return key header_key = ApiKey() @api.get("/headerkey" , auth=header_key ) def apikey (request ): return f"Token = {request.auth} "
in Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from ninja.security import APIKeyCookieclass CookieKey (APIKeyCookie ): def authenticate (self, request, key ): if key == "supersecret" : return key cookie_key = CookieKey() @api.get("/cookiekey" , auth=cookie_key ) def apikey (request ): return f"Token = {request.auth} "
HTTP Bearer 1 2 3 4 5 6 7 8 9 10 11 12 13 from ninja.security import HttpBearerclass AuthBearer (HttpBearer ): def authenticate (self, request, token ): if token == "supersecret" : return token @api.get("/bearer" , auth=AuthBearer( ) ) def bearer (request ): return {"token" : request.auth}
HTTP Basic Auth 1 2 3 4 5 6 7 8 9 10 11 12 13 from ninja.security import HttpBasicAuthclass BasicAuth (HttpBasicAuth ): def authenticate (self, request, username, password ): if username == "admin" and password == "secret" : return username @api.get("/basic" , auth=BasicAuth( ) ) def basic (request ): return {"httpuser" : request.auth}
多个验证器 auth 参数还允许您传递多个身份验证器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from ninja.security import APIKeyQuery, APIKeyHeaderclass AuthCheck : def authenticate (self, request, key ): if key == "supersecret" : return key class QueryKey (AuthCheck, APIKeyQuery): pass class HeaderKey (AuthCheck, APIKeyHeader): pass @api.get("/multiple" , auth=[QueryKey( ), HeaderKey( )] ) def multiple (request ): return f"Token = {request.auth} "
在这种情况下,Django Ninja 将首先检查 API 密钥 GET,如果未设置或无效,将检查标头密钥。如果两者都无效,则会向响应引发身份验证错误。
路由器认证 在 Router 上使用 auth 参数将验证器应用于其中声明的所有操作:
api.add_router(“/events/“, events_router, auth=BasicAuth())
或使用路由器构造函数:
router = Router(auth=BasicAuth())
自定义处理 引发具有异常处理程序的异常将从该处理程序返回响应,其方式与操作相同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from ninja import NinjaAPIfrom ninja.security import HttpBearerapi = NinjaAPI() class InvalidToken (Exception ): pass @api.exception_handler(InvalidToken ) def on_invalid_token (request, exc ): return api.create_response(request, {"detail" : "Invalid token supplied" }, status=401 ) class AuthBearer (HttpBearer ): def authenticate (self, request, token ): if token == "supersecret" : return token raise InvalidToken @api.get("/bearer" , auth=AuthBearer( ) ) def bearer (request ): return {"token" : request.auth}
异步认证 Django Ninja 对异步身份验证具有基本支持。虽然默认的身份验证类不兼容异步,但您仍然可以定义自定义异步身份验证可调用对象并使用 auth.
1 2 3 4 5 6 7 8 async def async_auth (request ): ... @api.get("/pets" , auth=async_auth ) def pets (request ): ...
异常处理 Django Ninja 允许安装自定义异常处理程序来处理发生错误或已处理异常时返回响应的方式。
自定义异常处理 如果你在写的 API 依赖于某些外部服务,而这些外部服务被设计为在某些时刻不可用。您可以处理错误并向客户端提供一些友好的响应(稍后返回),而不是在异常时抛出默认的 500 错误。 为此,可以:
创建一些异常(或使用现有的异常) 使用 api.exception_handler 装饰器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import randomfrom ninja import NinjaAPIapi = NinjaAPI() class ServiceUnavailableError (Exception ): pass @api.exception_handler(ServiceUnavailableError ) def service_unavailable (request, exc ): return api.create_response( request, {"message" : "Please retry later" }, status=503 , ) @api.get("/service" ) def some_operation (request ): if random.choice([True , False ]): raise ServiceUnavailableError() return {"message" : "Hello" }
异常处理函数有 2 个参数:
请求 - Django http 请求 exc - 实际异常 函数必须返回http响应。
覆盖默认的异常处理 默认情况下,Django Ninja 初始化了以下异常处理程序:ninja.errors.AuthenticationError 身份验证数据无效时引发
ninja.errors.ValidationError 当请求数据未验证时引发
ninja.errors.HttpError 用于从代码的任何位置抛出带有状态代码的http错误
django.http.Http404 Django 默认的 404 异常(可以使用 get_object_or_404 返回)
Exception 应用程序的任何其他未处理的异常。
默认行为:
if settings.DEBUG 为 True - 以纯文本形式返回回溯(在控制台或 swagger UI 中调试时很有用) else - 使用默认的 django 异常处理程序机制(错误日志记录、发送电子邮件至 ADMINS) 如果需要更改验证错误的默认输出 - 覆盖 ValidationError 异常处理程序:
1 2 3 4 5 6 from ninja.errors import ValidationError... @api.exception_handler(ValidationError ) def validation_errors (request, exc ): return HttpResponse("Invalid input" , status=422 )
抛出异常的 HTTP 响应 作为自定义异常和为其编写处理程序的替代方案 - 您也可以抛出 http 异常,这将导致返回带有所需代码的 http 响应
1 2 3 4 5 6 from ninja.errors import HttpError@api.get("/some/resource" ) def some_operation (request ): if True : raise HttpError(503 , "Service Unavailable. Please retry later." )
操作参数 OpenAPI模式相关 tags 您可以使用标签参数 (list[str]) 对 API 操作进行分组。
1 2 3 @api.post("/orders/" , tags=["orders" ] ) def create_order (request, order: Order ): return {"success" : True }
Router tags 可以使用标签参数将标签应用于路由器声明的所有操作:
1 2 3 4 5 6 api.add_router("/events/" , events_router, tags=["events" ]) router = Router(tags=["events" ])
summary 默认情况下,它是通过大写操作函数名称生成的。 如果您想覆盖它,请在 api 装饰器中使用摘要参数summary:
1 2 3 @api.get("/hello/" , summary="Say Hello" ) def hello (request, name: str ): return {"hello" : name}
description 要提供有关操作的更多信息,使用描述参数或普通的 Python 文档字符串:
1 2 3 @api.post("/orders/" , description="Creates an order and updates stock" ) def create_order (request, order: Order ): return {"success" : True }
需要提供长的多行描述时,可以使用 Python 文档字符串进行函数定义:
1 2 3 4 5 6 7 8 9 @api.post("/orders/" ) def create_order (request, order: Order ): """ To create an order please provide: - **first_name** - **last_name** - and **list of Items** *(product + amount)* """ return {"success" : True }
operation_id OpenAPI operationId 是一个可选的唯一字符串,用于标识操作。如果提供,这些 ID 在 API 中描述的所有操作中必须是唯一的。 默认情况下,Django Ninja 将其设置为模块名称+函数名称。 如果要为每个操作单独设置,请使用 operation_id 参数:
1 2 3 4 ... @api.post("/tasks" , operation_id="create_task" ) def new_task (request ): ...
如果要覆盖全局行为,可以继承 NinjaAPI 实例并覆盖 get_openapi_operation_id 方法。 您定义的每个操作都会调用它,因此您可以像这样设置自定义命名逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 from ninja import NinjaAPIclass MySuperApi (NinjaAPI ): def get_openapi_operation_id (self, operation ): return ... api = MySuperApi() @api.get(... ) ...
deprecated 使用 deprecated 参数将操作标记为已弃用,而不将其删除:
1 2 3 @api.post("/make-order/" , deprecated=True ) def some_old_method (request, order: str ): return {"success" : True }
include_in_schema 如果您需要从 OpenAPI 模式中包含/排除某些操作,请使用 include_in_schema 参数:
1 2 3 @api.post("/hidden" , include_in_schema=False ) def some_hidden_operation (request ): pass
您可以为特定端点自定义 OpenAPI 架构(详细信息 OpenAPI 自定义选项)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @api.get( "/tasks" , openapi_extra={ "requestBody" : { "content" : { "application/json" : { "schema" : { "required" : ["email" ], "type" : "object" , "properties" : { "name" : {"type" : "string" }, "phone" : {"type" : "number" }, "email" : {"type" : "string" }, }, } } }, "required" : True , } }, )def some_operation (request ): pass @api.post( "/tasks" , openapi_extra={ "responses" : { "400" : { "description" : "Error Response" , }, "404" : { "description" : "Not Found Response" , }, }, }, )def some_operation_2 (request ): pass
响应输出选项 by_alias 字段别名是否应用作响应中的键(默认为 False)。
exclude_unset 是否应从响应中排除创建架构时未设置且具有默认值的字段(默认为 False)。
exclude_defaults 是否应从响应中排除等于默认值(无论是否设置)的字段(默认为 False)。
exclude_none 是否应从响应中排除等于 None 的字段(默认为 False)。
url_name 允许您设置 api 端点 url 名称(使用 django 路径的命名)
1 2 3 4 5 6 7 @api.post("/tasks" , url_name='tasks' ) def some_operation (request ): pass reverse('api-1.0.0:tasks' )
指定服务器 如果要为 OpenAPI 规范指定单个或多个服务器,可以在初始化 NinjaAPI 实例时使用服务器:
1 2 3 4 5 6 7 8 from ninja import NinjaAPIapi = NinjaAPI( servers=[ {"url" : "https://stag.example.com" , "description" : "Staging env" }, {"url" : "https://prod.example.com" , "description" : "Production env" }, ] )
这将允许在使用交互式 OpenAPI 文档时在环境之间切换:
1 ![交互式文档](https://i0.wp.com/cdn.jsdelivr.net/gh/lenkyes/GitImgStore/uploads/2023 /07/20230717200231. png)