跳转到主要内容

使用Python注解生成OpenAPI文档并验证请求和响应。

项目描述

SpecTree

GitHub Actions pypi versions CodeQL Python document

另一个用于生成OpenAPI文档和用Python注解验证请求和响应的库。

如果您只需要一个无框架的库来生成OpenAPI文档,请检查defspec

特性

  • 更少的样板代码,仅注解,无需YAML :sparkles
  • 使用Redoc UIScalar UISwagger UI生成API文档 :yum
  • 使用pydantic验证查询、JSON数据和响应数据 :wink
    • 如果您正在使用Pydantic V2,您需要从pydantic.v1导入BaseModel以使其兼容
  • 当前支持

快速入门

使用pip安装: pip install spectree。如果您想要验证电子邮件字段,请使用pip install spectree[email]

示例

检查示例文件夹。

逐步操作

  1. 使用pydantic.BaseModel定义在(query, json, headers, cookies, resp)中使用的数据结构
  2. 使用您正在使用的Web框架名称创建spectree.SpecTree实例,例如api = SpecTree('flask')
  3. api.validate装饰器用于路由(默认值在括号内给出)
    • 查询
    • JSON
    • 头部
    • Cookie
    • 响应
    • tags (端点上没有标签)
    • security (None - 端点未加密)
    • deprecated (False - 端点未标记为已弃用)
  4. 使用context(query, json, headers, cookies)访问这些数据(当然,您也可以从框架提供原始位置访问)
    • Flask: request.context
    • Falcon: req.context
    • Starlette: request.context
  5. 使用api.register(app)将它们注册到Web应用程序
  6. 在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对象的数组。然后有两种方式来强制执行安全性

  1. 您可以通过在相关函数/方法的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():
    ...

  1. 您可以通过在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()装饰器中使用带有值Truedeprecated属性。这样,端点将被标记为已弃用,并在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文件。

当出现验证错误时,我该如何更改响应?我可以记录一些度量指标吗?

此库提供了beforeafter钩子来完成这些操作。请查看文档测试用例。您可以更改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框架的解决方案。因此,我建议不要在查询中使用列表类型,直到我找到合适的解决方案来修复它。

项目详情


发布历史 发布通知 | RSS源

下载文件

下载适合您平台文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。

源代码分发

spectree-1.2.10.tar.gz (54.2 kB 查看哈希值)

上传时间 源代码

构建分发

spectree-1.2.10-py3-none-any.whl (41.9 kB 查看哈希值)

上传时间 Python 3

支持者: