使用Python注解生成OpenAPI文档并验证请求和响应。
项目描述
SpecTree
另一个用于生成OpenAPI文档和用Python注解验证请求和响应的库。
如果您只需要一个无框架的库来生成OpenAPI文档,请检查defspec。
特性
- 更少的样板代码,仅注解,无需YAML :sparkles
- 使用Redoc UI,Scalar UI或Swagger UI生成API文档 :yum
- 使用pydantic验证查询、JSON数据和响应数据 :wink
- 如果您正在使用Pydantic V2,您需要从
pydantic.v1
导入BaseModel
以使其兼容
- 如果您正在使用Pydantic V2,您需要从
- 当前支持
快速入门
使用pip安装: pip install spectree
。如果您想要验证电子邮件字段,请使用pip install spectree[email]
。
示例
检查示例文件夹。
逐步操作
- 使用
pydantic.BaseModel
定义在(query, json, headers, cookies, resp)中使用的数据结构 - 使用您正在使用的Web框架名称创建
spectree.SpecTree
实例,例如api = SpecTree('flask')
api.validate
装饰器用于路由(默认值在括号内给出)查询
JSON
头部
Cookie
响应
tags
(端点上没有标签)security
(None
- 端点未加密)deprecated
(False
- 端点未标记为已弃用)
- 使用
context(query, json, headers, cookies)
访问这些数据(当然,您也可以从框架提供原始位置访问)- Flask:
request.context
- Falcon:
req.context
- Starlette:
request.context
- Flask:
- 使用
api.register(app)
将它们注册到Web应用程序 - 在URL位置检查文档
/apidoc/redoc
或/apidoc/swagger
或/apidoc/scalar
如果请求未通过验证,将返回一个包含JSON错误消息(ctx, loc, msg, type)的422错误
Falcon响应验证
对于Falcon响应,此库仅验证媒体,因为它是一个可序列化的对象。Response.text是表示响应内容的字符串,不会进行验证。对于未分配媒体的情况,api.validate
中的resp
参数应如下所示:Response(HTTP_200=None)
选择启用类型注解功能
此库还支持将验证字段注入到视图函数参数中,同时使用基于参数注解的类型声明。这对于可以利用类型功能的linters,如mypy,效果很好。请参阅下面的示例部分。
如何做
如何添加端点的摘要和描述?
只需将文档添加到端点函数中。第一行是摘要,其余部分是该端点的描述。
如何添加参数的描述?
查看关于Field
中描述的pydantic文档。
我可以更改任何配置吗?
当然。查看配置文档。
您可以在初始化spectree时更新配置
SpecTree('flask', title='Demo API', version='v1.0', path='doc')
Response
是什么以及如何使用它?
要为端点构建响应,您需要声明状态码,格式为HTTP_{code}
以及相应的数据(可选)。
Response(HTTP_200=None, HTTP_403=ForbidModel)
Response('HTTP_200') # equals to Response(HTTP_200=None)
# with custom code description
Response(HTTP_403=(ForbidModel, "custom code description"))
如何保护API端点?
对于受保护的API端点,需要在SpecTree
构造函数中定义security_schemes
参数。需要包含一个包含SecurityScheme
对象的数组。security_schemes
参数需要包含一个包含SecurityScheme
对象的数组。然后有两种方式来强制执行安全性
- 您可以通过在相关函数/方法的
api.validate
装饰器中定义security
参数来对单个API端点执行安全性强制(这对应于在OpenAPI
中操作级别下的paths
部分中定义安全部分)。security
参数定义为一个字典,其中的每个键是SpecTree
构造函数中security_schemes
参数中使用的安全名称,其值是所需的安全范围,如下例所示
点击展开代码示例
api = SpecTree(security_schemes=[
SecurityScheme(
name="auth_apiKey",
data={"type": "apiKey", "name": "Authorization", "in": "header"},
),
SecurityScheme(
name="auth_oauth2",
data={
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://example.com/oauth/authorize",
"tokenUrl": "https://example.com/oauth/token",
"scopes": {
"read": "Grants read access",
"write": "Grants write access",
"admin": "Grants access to admin operations",
},
},
},
},
),
# ...
],
# ...
)
# Not secured API endpoint
@api.validate(
resp=Response(HTTP_200=None),
)
def foo():
...
# API endpoint secured by API key type or OAuth2 type
@api.validate(
resp=Response(HTTP_200=None),
security={"auth_apiKey": [], "auth_oauth2": ["read", "write"]}, # Local security type
)
def bar():
...
- 您可以通过在
SpecTree
构造函数中定义security
参数来对整个API执行安全性强制(这对应于在OpenAPI
中根级别下定义安全部分)。可以通过定义局部安全性来覆盖全局安全性,以及通过在相关函数/方法的api.validate
装饰器的security
参数中覆盖某些API端点上的无安全性,如前所述。以下是一个小示例
点击展开代码示例
api = SpecTree(security_schemes=[
SecurityScheme(
name="auth_apiKey",
data={"type": "apiKey", "name": "Authorization", "in": "header"},
),
SecurityScheme(
name="auth_oauth2",
data={
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://example.com/oauth/authorize",
"tokenUrl": "https://example.com/oauth/token",
"scopes": {
"read": "Grants read access",
"write": "Grants write access",
"admin": "Grants access to admin operations",
},
},
},
},
),
# ...
],
security={"auth_apiKey": []}, # Global security type
# ...
)
# Force no security
@api.validate(
resp=Response(HTTP_200=None),
security={}, # Locally overridden security type
)
def foo():
...
# Force another type of security than global one
@api.validate(
resp=Response(HTTP_200=None),
security={"auth_oauth2": ["read"]}, # Locally overridden security type
)
def bar():
...
# Use the global security
@api.validate(
resp=Response(HTTP_200=None),
)
def foobar():
...
如何标记已弃用的端点?
在api.validate()
装饰器中使用带有值True
的deprecated
属性。这样,端点将被标记为已弃用,并在API文档中带有删除线。
代码示例
@api.validate(
deprecated=True,
)
def deprecated_endpoint():
...
使用此库时我应该返回什么?
无需更改任何内容。只需返回框架要求的即可。
如何在验证失败时进行日志记录?
验证错误以INFO级别进行记录。详细信息传递到extra
中。有关详细信息,请查看falcon示例。
我该如何为另一个后端框架编写自定义插件?
继承spectree.plugins.base.BasePlugin
并实现所需的函数。然后,像这样初始化:api = SpecTree(backend=MyCustomizedPlugin)
。
如何使用自定义模板页面?
SpecTree(page_templates={"page_name": "customized page contains {spec_url} for rendering"})
在上面的示例中,键"page_name"将用于访问此页面"/apidoc/page_name"。值应该是包含{spec_url}
的字符串,该字符串将用于访问OpenAPI JSON文件。
当出现验证错误时,我该如何更改响应?我可以记录一些度量指标吗?
此库提供了before
和after
钩子来完成这些操作。请查看文档或测试用例。您可以更改SpecTree或特定端点的处理程序。
如何更改默认的
ValidationError
状态码?
您可以在SpecTree(全局)或特定端点(局部)中更改validation_error_status
。这也会在OpenAPI文档中生效。
我该如何跳过验证?
将skip_validation=True
添加到装饰器中。目前,这仅跳过响应验证。
@api.validate(json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), skip_validation=True)
我该如何直接返回我的模型?
是的,返回BaseModel
的实例将假定模型是有效的,并绕过spectree的验证,并自动调用模型上的.dict()
。
对于starlette,您应该返回一个PydanticResponse
from spectree.plugins.starlette_plugin import PydanticResponse
return PydanticResponse(MyModel)
演示
尝试使用http post :8000/api/user name=alice age=18
进行测试。(如果您使用的是httpie
)
Flask
from flask import Flask, request, jsonify
from pydantic import BaseModel, Field, constr
from spectree import SpecTree, Response
class Profile(BaseModel):
name: constr(min_length=2, max_length=40) # constrained str
age: int = Field(..., gt=0, lt=150, description="user age(Human)")
class Config:
schema_extra = {
# provide an example
"example": {
"name": "very_important_user",
"age": 42,
}
}
class Message(BaseModel):
text: str
app = Flask(__name__)
spec = SpecTree("flask")
@app.route("/api/user", methods=["POST"])
@spec.validate(
json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"]
)
def user_profile():
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(request.context.json) # or `request.json`
return jsonify(text="it works") # or `Message(text='it works')`
if __name__ == "__main__":
spec.register(app) # if you don't register in api init step
app.run(port=8000)
带有类型注解的Flask示例
# opt in into annotations feature
spec = SpecTree("flask", annotations=True)
@app.route("/api/user", methods=["POST"])
@spec.validate(resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"])
def user_profile(json: Profile):
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(json) # or `request.json`
return jsonify(text="it works") # or `Message(text='it works')`
Quart
from quart import Quart, jsonify, request
from pydantic import BaseModel, Field, constr
from spectree import SpecTree, Response
class Profile(BaseModel):
name: constr(min_length=2, max_length=40) # constrained str
age: int = Field(..., gt=0, lt=150, description="user age")
class Config:
schema_extra = {
# provide an example
"example": {
"name": "very_important_user",
"age": 42,
}
}
class Message(BaseModel):
text: str
app = Quart(__name__)
spec = SpecTree("quart")
@app.route("/api/user", methods=["POST"])
@spec.validate(
json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"]
)
async def user_profile():
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(request.context.json) # or `request.json`
return jsonify(text="it works") # or `Message(text="it works")`
if __name__ == "__main__":
spec.register(app)
app.run(port=8000)
带有类型注解的Quart示例
# opt in into annotations feature
spec = SpecTree("quart", annotations=True)
@app.route("/api/user", methods=["POST"])
@spec.validate(resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"])
def user_profile(json: Profile):
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(json) # or `request.json`
return jsonify(text="it works") # or `Message(text='it works')`
Falcon
import falcon
from wsgiref import simple_server
from pydantic import BaseModel, Field, constr
from spectree import SpecTree, Response
class Profile(BaseModel):
name: constr(min_length=2, max_length=40) # Constrained Str
age: int = Field(..., gt=0, lt=150, description="user age(Human)")
class Message(BaseModel):
text: str
spec = SpecTree("falcon")
class UserProfile:
@spec.validate(
json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"]
)
def on_post(self, req, resp):
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(req.context.json) # or `req.media`
resp.media = {"text": "it works"} # or `resp.media = Message(text='it works')`
if __name__ == "__main__":
app = falcon.App()
app.add_route("/api/user", UserProfile())
spec.register(app)
httpd = simple_server.make_server("localhost", 8000, app)
httpd.serve_forever()
带有类型注解的Falcon
# opt in into annotations feature
spec = SpecTree("falcon", annotations=True)
class UserProfile:
@spec.validate(resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"])
def on_post(self, req, resp, json: Profile):
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(req.context.json) # or `req.media`
resp.media = {"text": "it works"} # or `resp.media = Message(text='it works')`
Starlette
import uvicorn
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.responses import JSONResponse
from pydantic import BaseModel, Field, constr
from spectree import SpecTree, Response
# from spectree.plugins.starlette_plugin import PydanticResponse
class Profile(BaseModel):
name: constr(min_length=2, max_length=40) # Constrained Str
age: int = Field(..., gt=0, lt=150, description="user age(Human)")
class Message(BaseModel):
text: str
spec = SpecTree("starlette")
@spec.validate(
json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"]
)
async def user_profile(request):
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(request.context.json) # or await request.json()
return JSONResponse(
{"text": "it works"}
) # or `return PydanticResponse(Message(text='it works'))`
if __name__ == "__main__":
app = Starlette(
routes=[
Mount(
"api",
routes=[
Route("/user", user_profile, methods=["POST"]),
],
)
]
)
spec.register(app)
uvicorn.run(app)
带有类型注解的Starlette示例
# opt in into annotations feature
spec = SpecTree("flask", annotations=True)
@spec.validate(resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"])
async def user_profile(request, json=Profile):
"""
verify user profile (summary of this endpoint)
user's name, user's age, ... (long description)
"""
print(request.context.json) # or await request.json()
return JSONResponse({"text": "it works"}) # or `return PydanticResponse(Message(text='it works'))`
常见问题解答
ValidationError:缺少标题字段
Flask中的HTTP标题键是大写,Falcon中是全部大写,Starlette中是全部小写。您可以使用pydantic.root_validators(pre=True)
将所有键更改为小写或大写。
ValidationError:查询值不是有效的列表
由于HTTP查询中没有多个值的规范,因此很难找到一种适合不同Web框架的解决方案。因此,我建议不要在查询中使用列表类型,直到我找到合适的解决方案来修复它。
项目详情
下载文件
下载适合您平台文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。
源代码分发
构建分发
spectree-1.2.10.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | b826f7738f23ab679e83ee5061369f15c53f798ac093e0efbe8f8b66e9943594 |
|
MD5 | f287a65de05437819553d9b853254a8d |
|
BLAKE2b-256 | 4ea6ae66f91c210d15fa99582d1bf4cb6998971978613c813c5c2bcb2e2ab65c |
spectree-1.2.10-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 45ef228c02d9eec413a4d2056ef4d3b8e530d3e075c2c69f92b1f86f3a9c003e |
|
MD5 | a366fffc831ca60b22ee7c4870c5e528 |
|
BLAKE2b-256 | 22b0d6311e8189da850b408fe3cc553777eb45fbe9fa622ae3101e1d6d0a7245 |