Giter VIP home page Giter VIP logo

spectree's Introduction

SpecTree

GitHub Actions pypi versions CodeQL Python document

Yet another library to generate OpenAPI documents and validate requests & responses with Python annotations.

If all you need is a framework-agnostic library that can generate OpenAPI document, check defspec.

Features

  • Less boilerplate code, only annotations, no need for YAML ✨
  • Generate API document with Redoc UI, Scalar UI or Swagger UI 😋
  • Validate query, JSON data, response data with pydantic 😉
    • If you're using Pydantic V2, you will need to import the BaseModel from pydantic.v1 to make it compatible
  • Current support:

Quick Start

Install with pip: pip install spectree. If you'd like for email fields to be validated, use pip install spectree[email].

Examples

Check the examples folder.

Step by Step

  1. Define your data structure used in (query, json, headers, cookies, resp) with pydantic.BaseModel
  2. create spectree.SpecTree instance with the web framework name you are using, like api = SpecTree('flask')
  3. api.validate decorate the route with (the default value is given in parentheses):
    • query
    • json
    • headers
    • cookies
    • resp
    • tags (no tags on endpoint)
    • security (None - endpoint is not secured)
    • deprecated (False - endpoint is not marked as deprecated)
  4. access these data with context(query, json, headers, cookies) (of course, you can access these from the original place where the framework offered)
    • flask: request.context
    • falcon: req.context
    • starlette: request.context
  5. register to the web application api.register(app)
  6. check the document at URL location /apidoc/redoc or /apidoc/swagger or /apidoc/scalar

If the request doesn't pass the validation, it will return a 422 with a JSON error message(ctx, loc, msg, type).

Falcon response validation

For Falcon response, this library only validates against media as it is the serializable object. Response.text is a string representing response content and will not be validated. For no assigned media situation, resp parameter in api.validate should be like Response(HTTP_200=None)

Opt-in type annotation feature

This library also supports the injection of validated fields into view function arguments along with parameter annotation-based type declaration. This works well with linters that can take advantage of typing features like mypy. See the examples section below.

How-To

How to add summary and description to endpoints?

Just add docs to the endpoint function. The 1st line is the summary, and the rest is the description for this endpoint.

How to add a description to parameters?

Check the pydantic document about description in Field.

Any config I can change?

Of course. Check the config document.

You can update the config when init the spectree like:

SpecTree('flask', title='Demo API', version='v1.0', path='doc')

What is Response and how to use it?

To build a response for the endpoint, you need to declare the status code with format HTTP_{code} and corresponding data (optional).

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"))

How to secure API endpoints?

For secure API endpoints, it is needed to define the security_schemes argument in the SpecTree constructor. security_schemes argument needs to contain an array of SecurityScheme objects. Then there are two ways to enforce security:

  1. You can enforce security on individual API endpoints by defining the security argument in the api.validate decorator of relevant function/method (this corresponds to define security section on operation level, under paths, in OpenAPI). security argument is defined as a dictionary, where each key is the name of security used in security_schemes argument of SpecTree constructor and its value is required security scope, as is showed in the following example:
Click to expand the code example:

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. You can enforce security on the whole API by defining the security argument in the SpecTree constructor (this corresponds to the define security section on the root level in OpenAPI). It is possible to override global security by defining local security, as well as override to no security on some API endpoint, in the security argument of api.validate decorator of relevant function/method as was described in the previous point. It is also shown in the following small example:
Click to expand the code example:

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():
    ...

How to mark deprecated endpoint?

Use deprecated attribute with value True in api.validate() decorator. This way, an endpoint will be marked as deprecated and will be marked with a strikethrough in API documentation.

Code example:

@api.validate(
    deprecated=True,
)
def deprecated_endpoint():
    ...

What should I return when I'm using the library?

No need to change anything. Just return what the framework required.

How to log when the validation failed?

Validation errors are logged with the INFO level. Details are passed into extra. Check the falcon example for details.

How can I write a customized plugin for another backend framework?

Inherit spectree.plugins.base.BasePlugin and implement the functions you need. After that, init like api = SpecTree(backend=MyCustomizedPlugin).

How to use a customized template page?

SpecTree(page_templates={"page_name": "customized page contains {spec_url} for rendering"})

In the above example, the key "page_name" will be used in the URL to access this page "/apidoc/page_name". The value should be a string that contains {spec_url} which will be used to access the OpenAPI JSON file.

How can I change the response when there is a validation error? Can I record some metrics?

This library provides before and after hooks to do these. Check the doc or the test case. You can change the handlers for SpecTree or a specific endpoint validation.

How to change the default ValidationError status code?

You can change the validation_error_status in SpecTree (global) or a specific endpoint (local). This also takes effect in the OpenAPI documentation.

How can I skip the validation?

Add skip_validation=True to the decorator. For now, this only skip the response validation.

@api.validate(json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), skip_validation=True)

How can I return my model directly?

Yes, returning an instance of BaseModel will assume the model is valid and bypass spectree's validation and automatically call .dict() on the model.

For starlette you should return a PydanticResponse:

from spectree.plugins.starlette_plugin import PydanticResponse

return PydanticResponse(MyModel)

Demo

Try it with http post :8000/api/user name=alice age=18. (if you are using 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 example with type annotation

# 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 example with type annotation

# 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 with type annotations

# 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 example with type annotations

# 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'))`

FAQ

ValidationError: missing field for headers

The HTTP headers' keys in Flask are capitalized, in Falcon are upper cases, in Starlette are lower cases. You can use pydantic.root_validators(pre=True) to change all the keys into lower cases or upper cases.

ValidationError: value is not a valid list for the query

Since there is no standard for HTTP queries with multiple values, it's hard to find a way to handle this for different web frameworks. So I suggest not to use list type in query until I find a suitable way to fix it.

spectree's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spectree's Issues

[BUG] Incorrect display of fields when schemes have the same names

Describe the bug
When serializers schemes (not only for req/resp for one endpoint) have the same name (it is actual too when nested models of schemes for different endpoints have the same names), the /redoc and /swagger does not display the field data correctly.

To Reproduce

base

from pydantic import BaseModel, Extra


class BaseSerializer(BaseModel):
    class Config:
        extra = Extra.forbid

endpoint /post

for req

from .base import BaseSerializer

...

class Passengers(BaseSerializer):
    type: str = Field(..., description="Возрастная категория пассажира", example="ADULT")


class CreateCartReq(BaseSerializer):
    cart_id: UUID4 = Field(
        ..., description="Уникальный идентификатор корзины", example="c205de04-e789-49d1-b232-4efb741a4e60"
    )
    passengers: List[Passengers] = Field(..., description="Информация о пассажирах", example=[{"type": "ADULT"}])
    order: Order = Field(..., description="Информация о заказе")
    updated_at: Optional[int] = Field(description="[old] Timestamp, последнего обновления", example=1619167989)

for resp

from .base import BaseSerializer

...

class Passengers(BaseSerializer):
    id: UUID4 = Field(
        ..., description="Уникальный идентификатор пассажира", example="63f84506-d626-4ca0-9247-eb453365ac92"
    )
    type: str = Field(..., description="Возрастная категория пассажира", example="ADULT")

class CreateCartResp(BaseSerializer):
    adult_count: Optional[int] = Field(description="[async] Количество взрослых", example=1)
    ...
    passengers: List[Passengers] = Field(
        ...,
        description="Данные о пассажирах",
        example=[{"id": "63f84506-d626-4ca0-9247-eb453365ac92", "type": "ADULT"}],
    )
    ...
    return_date: Optional[int] = Field(description="[async] Дата возвращения", example=1622505600)

endpoint /put

for req

from .base import BaseSerializer

...

class Passengers(BaseSerializer):
    id: UUID4 = Field(..., description="Уникальный идентификатор", example="507a72ef-3af6-4f30-916d-f914485d1606")
    doctype: str = Field(..., description="Тип документа удостоверяющего личность", example="ps")
    docnumber: str = Field(..., description="Серия и номер документа", example="1401815424")
    doccountry: NoneStr = Field(description="Код страны документа", example="RU")
    docexpiration: Optional[int] = Field(description="Срок действия документа", example=1861920000)
    birthday: int = Field(..., description="Timestamp даты рождения", example=691977600)
    gender: str = Field(..., description="Пол", example="M")
    first_name: str = Field(..., description="Имя", example="Иван")
    last_name: str = Field(..., description="Фамилия", example="Иванов")
    second_name: NoneStr = Field(description="Отчество", example="Иванович")
    type: str = Field(..., description="Возрастная категория пассажира", example="ADULT")
    ff_cardnumber: NoneStr = Field(description="Номер карты лояльности", example="1025060987")


class UpdateCartReq(BaseSerializer):
    adult_count: Optional[int] = Field(description="[old] Количество взрослых", example=1)
    ...
    passengers: List[Passengers] = Field(..., description="Информация о пассажирах")
    ...
    user: Optional[User] = Field(
        description="Уникальный идентификатор пользователя", example={"id": "5b71cbffba3e9c584cb1a5c2"}
    )

for resp

from .base import BaseSerializer

...

class Passengers(BaseSerializer):
    birthday: int = Field(..., description="Timestamp даты рождения", example=691977600)
    doccountry: NoneStr = Field(description="Код страны документа", example="RU")
    docnumber: str = Field(..., description="Серия и номер документа", example="1401815424")
    doctype: str = Field(..., description="Тип документа удостоверяющего личность", example="ps")
    docexpiration: Optional[int] = Field(description="Срок действия документа", example=1861920000)
    first_name: str = Field(..., description="Имя", example="Иван")
    gender: str = Field(..., description="Пол", example="M")
    id: str = Field(..., description="Уникальный идентификатор", example="507a72ef-3af6-4f30-916d-f914485d1606")
    last_name: str = Field(..., description="Фамилия", example="Иванов")
    second_name: NoneStr = Field(description="Отчество", example="Иванович")
    type: str = Field(..., description="Возрастная категория пассажира", example="ADULT")
    ff_cardnumber: NoneStr = Field(description="Номер карты лояльности", example="1025060987")


class UpdateCartResp(BaseSerializer):
    adult_count: Optional[int] = Field(description="[old] Количество взрослых", example=1)
    ...
    passengers: List[Passengers] = Field(..., description="Информация о пассажирах")
    ...
    user: Optional[User] = Field(
        description="Уникальный идентификатор пользователя", example={"id": "5b71cbffba3e9c584cb1a5c2"}
    )

Expected behavior
Correct display of the serializers schemes fields.

Actual behavior
if schemes in one endpoint have the same names
first-error-screen
if schemes in several endpoints have the same names
second-error-screen

Desktop:

  • OS: Linux
  • Version: Manjaro 21.0.5

Python Information:

  • Python Version: 3.6
  • Library Version: 0.4.3
  • Other dependencies: flask=1.1.4

Additional context
The problem is also observed, not only within a single endpoint, but also for several endpoints, if their serializers schemes contains (nested) schemes with the same name.

[BUG] validation of array[]-like parameter in query string

Describe the bug
Hi,
consider the following model and request:

class StatsQuery(BaseModel):
    events: List[Literal["CREATED", "DEACTIVATED"]]

/stats?events[]=CREATED&events[]=DEACTIVATED

edit above:

/stats?events=CREATED&events=DEACTIVATED

Expected behavior

  • request validation will pass

Error Message

[
  {
    "loc": [
      "events"
    ], 
    "msg": "value is not a valid list", 
    "type": "type_error.list"
  }
]

Python Information (please complete the following information):

  • Python 3.8.0
  • spectree==0.3.0
  • Flask==1.1.1

Additional context

For now I'm using this ugly wokaround: in FlaskPlugin.validate():

-arg = request.args or {}
+arg = request.args.to_dict(flat=True) or {}
+if arg:
+    for field_name, field_info in query.schema()['properties'].items():
+        if field_info['type'] == 'array':
+            arg[field_name] = request.args.getlist(field_name)

[BUG] There is no way to create swagger file upload

Describe the bug
As json takes a pynatic model and there is no Pyndatic field with type file I could not find a way to upload a file.

Does anybody know of a way to create a swagger file upload in Flask?

[BUG] Pydantic custom root doesn't work as expected

Describe the bug
Using __root__ from pydantic doesn't work in falcon (json).

To Reproduce
Create a Model with custom __root__ as described in https://pydantic-docs.helpmanual.io/usage/models/#custom-root-types.

Example:

class Model(BaseModel):
    __root__: Dict[str, str]

It seems that spectree doesn't recognize __root__ as a special entry.

{'a': 'b'} # Doesn't work

{'__root__': {'a': 'b'}} # Does work

Desktop (please complete the following information):

  • OS: Windows
  • Version 10

Python Information (please complete the following information):

  • Python Version Python=3.6
  • Library Version spectree=0.3.15
  • Other dependencies falcon=3.0.1a

I'm currently using WSGI (waitress-serve) with falcon.

test

  • utils
  • Response
  • plugins
  • spec
  • validate

`@api.validate` seems to not work with starletee class endpoint

Hi.

I have this starlette endpoint.

class UserPostListEndpoint(HTTPEndpoint):
    @api.validate(resp=Response(HTTP_200=PostListSchema))
    async def get(self, request):
        user_id = request.path_params["user_id"]
        return UJSONResponse(await get_user_posts(user_id))

This endpoint is mounted as:

    routes = [
        Mount(
            "/api/v1",
            name="v1",
            routes=[
                Route(
                    "/users/{user_id:int}/posts", UserPostListEndpoint, name="user_list"
                )
            ],
        )
    ]

Invoking it throws an exception.

, in run
    result = self.fn(*self.args, **self.kwargs)
TypeError: validate() missing 2 required positional arguments: 'receive' and 'send'

Does this not work with starlette endpoint?

[Feature] Delete UnprocessableEntity from Response class

In __init__ method of Response class, we have the following code

if code_models and "HTTP_422" not in code_models:
    code_models["HTTP_422"] = UnprocessableEntity

It creates UnprocessableEntity and add it into response.

"responses":{
   "200":{
      "description":"OK",
      "content":{
         "application/json":{
            "schema":{
               "$ref":"#/components/schemas/BaseResponse"
            }
         }
      }
   },
   "422":{
      "description":"Unprocessable Entity",
      "content":{
         "application/json":{
            "schema":{
               "$ref":"#/components/schemas/UnprocessableEntity"
            }
         }
      }
   }
}

Maybe there is another way to not add 422 code into response?
@api.validate(json=BaseRequest, resp=Response(HTTP_200=BaseResponse, HTTP_422=None))
doesn't create schema but add 422 code into response

"responses":{
   "422":{
      "description":"Unprocessable Entity"
   },
   "200":{
      "description":"OK",
      "content":{
         "application/json":{
            "schema":{
               "$ref":"#/components/schemas/BaseResponse"
            }
         }
      }
   }
}

Refactor plugin structure

related to #21 #25

Reason

  • Falcon uses the instance of classes while Starlette endpoint uses the classes (this may not affect this library)
  • Starlette supports functions and HTTPEndPoint classes (sync & async), which makes it hard to implement a general validator
  • Starlette functions decorator will get (scope, receive, send) instead of (request)
  • wraps functions and methods is a pain point of Python
  • partial will return a class with the wrapped function, but Starlette will check whether the function is async or not
  • async errors are really hard to debug
  • multilayers function wrap may not be a good idea

[New Feature] Add a description to the tag

I read the source code of tags:

...
                for tag in func_tags:
                    if tag not in tags:
                        tags[tag] = {"name": tag}
...

https://github.com/0b01001001/spectree/blob/v0.4.2/spectree/spec.py#L212

Please consider adding a description field to the tag(Because I want to describe the tag in Chinese),just like official documents:
https://swagger.io/specification/

. . .

Tag Object

Adds metadata to a single tag that is used by the Operation Object. It is not mandatory to have a Tag Object per tag defined in the Operation Object instances.

Fixed Fields

Field Name Type Description
name string REQUIRED. The name of the tag.
description string A short description for the tag. CommonMark syntax MAY be used for rich text representation.
externalDocs External Documentation Object Additional external documentation for this tag.

. . .

[BUG] The form with the same name will be overwritten

Describe the bug
The form with the same name will be overwritten,even in different modules

To Reproduce
Steps to reproduce the behavior:
/forms1/form1.py

from pydantic import BaseModel, Field

class MyReq(BaseModel):
   param2: str = Field(description='This is param2')

class MyResp(BaseModel):
   text2: str

/forms2/form2.py

from pydantic import BaseModel, Field

class MyReq(BaseModel):
    param1: int = Field(description='This is param1')

class MyResp(BaseModel):
    text1: str

app

@app.route('/api/test1')
@api.validate(query=form1.MyReq, resp=Response(HTTP_200=form1.MyResp), tags=['api-get'])
def test_get1(query: form1.MyReq):
    """test_get2 doc"""
    return jsonify(text='get1 works')

@app.route('/api/test2')
@api.validate(query=form2.MyReq, resp=Response(HTTP_200=form2.MyResp), tags=['api-get'])
def test_get2(query: form2.MyReq):
    """test_get2 doc"""
    return jsonify(text='get2 works')

Expected behavior
The query parameters of test1 and test2 should be different,because they are different forms with the same name.
However, the query parameters are the same as form2.MyReq.
image

Error Message
Here is the code in spec.py

# register
for name, model in zip(
    ("query", "json", "headers", "cookies"), (query, json, headers, cookies)
):
    if model is not None:
        assert issubclass(model, BaseModel)
        self.models[model.__name__] = model.schema(
            ref_template="#/components/schemas/{model}"
        )
        setattr(validation, name, model.__name__)

if resp:
    for model in resp.models:
        self.models[model.__name__] = model.schema(
            ref_template="#/components/schemas/{model}"
        )

I found the problem is the key of models which is model.__name__, is this the excepted result?

Python Information (please complete the following information):

  • Python Version [Python=3.7]
  • Library Version [spectree=0.4.3]
  • Other dependencies [flask=1.1.2, pydantic = 1.8.1]

[BUG]can not get params by application/x-www-form-urlencoded

when frontend pass params using application/x-www-form-urlencoded, spectree can not get params by query, is there any other ways to get it?
截屏2020-11-18 上午10 42 59
截屏2020-11-18 上午10 48 06
截屏2020-11-18 上午10 48 24

I just find the code using request.args and request.json to get params, however request.values or request.form not use.
截屏2020-11-18 上午10 37 00

flask-restful problem: request body and parameters doesn't appear in documentation

Screenshot from 2021-01-02 17-25-23

When I decorate post method in resource class with api.validate(json=User) , request body and parameters doesn't appear in documentation

@api.validate(json=User, resp=Response(HTTP_200=None, HTTP_400=None), tags=['UserSignUp'])
    def post(self):
        print(reqparse.request.context.json)
        self.__add_parser_argument()
        user_args = self.parser.parse_args()

init spectree with plugin instead of name, use `body` instead of `json`

To keep the interface clean and flexible, it's better to pass the plugin class instead of backend framework name strings. Also, the sync/async flag should be written to the plugin class as described in https://github.com/0b01001001/spectree/pull/46/files#diff-485ad20a22f45089777317b26137dc90R13-R15

Since the body can be serialized in multiple ways, like JSON, MessagePack, ProtoBuf, etc. Although JSON should be the most common way to do so, it's better to support other methods.

falcon endpoint function doesn't get the right `self`

This is due to the decorator.

class Demo:
    def test(self): pass
    def on_get(self, req, resp):
        self.test() # this will raise AttributeError
        pass

And the parse_name function for falcon endpoint function should return the class name instead of the method name. Since the method names are all the same.

[Feature]Can't support list type swagger response

like:

class Item(BaseModel):
    name: str
    price: float


@app.route("/", method=["POST"])
@api.validate(json=Item, resp=Response(HTTP_200=List[Item]), tags=["demo"])
def demo():
    item = Item.parse_obj(request.context.json)
    return [item, item, item]

Feedback

Thank you for this library, this will be very useful for people who wants to use pydantic in their APIs.

I had some issues when I tried the lib:

  • The JSON body is always requested in swagger/redoc in every endpoint even if it's a GET or need no body
  • The headers are working for me when I use postman, but in documentation they are shown as an object and they are not passed to endpoints.
  • Can you add option to select what documentation to expose at registration step, and if possible option to change the doc url
  • I guess you are validating the response even if there is no validation is set up : The view function did not return a valid response. The return type must be a string, tuple, Response instance, or WSGI callable, but it was a int.. I think you have to keep everything optional.
  • If possible to add description to each endpoint

Async Falcon support?

I'm currently experimenting with Falcon 3.0.0a1 and was wondering if you have plans to add an async Falcon plugin for SpecTree (3.0 will support both WSGI and ASGI applications)?

If not, this might be something I would be interesting in contributing since I really like that SpecTree provides a way to both handle input/output (de)serialization with Pydantic and autogenerates an OpenAPI document similar to FastAPI. What would your preferred method of implementing this? A completely separate plugin, a subclass of the existing Falcon plugin, something else?

[Feature request] Make the validation error more verbose

It is difficult to tell why validation error(s) happend because Spectree only returns "response validation error".

                try:
                    model.validate(response.get_json())
                except ValidationError as err:
                    resp_validation_error = err
                    response = make_response(
                        jsonify({"message": "response validation error"}), 500
                    )

(https://github.com/0b01001001/spectree/blob/master/spectree/plugins/flask_plugin.py#L156-L160)

How about making the message more verbose?

For example, FastAPI returns a verbose validation error response.
https://github.com/tiangolo/fastapi/blob/e1758d107ea5eede358cfdbf69ae7829c8e65a92/fastapi/openapi/utils.py#L33-L42

Openapi description for path variable

So far, I can't find a way to add a description for path variables. On the flask side, It would be cool to have a parameter such doc as flask-restfull provides :

@api.route('/my-resource/<id>', endpoint='my-resource')
@api.doc(params={'id': 'An ID'})

[BUG]Openapi.json cannot be properly loaded when host includes sub-domain

Describe the bug
Openapi.json cannot be properly loaded when host includes sub-domain

To Reproduce
For example:
Given the hostname: http://123.4.0.0/api/
Access the doc: http://123.4.0.0/api/apidoc/swagger
The Swagger UI will attempt to load the openapi.json at /apidoc/openapi.json where the json does not exist

Expected behavior
The Swagger UI should attempt to load the openapi.json at:
/api/apidoc/openapi.json

Error Message
Unable to render definition

Others
The test feature is also unusable, because the Swagger UI use the wrong Request-URL:
http://123.4.0.0 instead of http://123.4.0.0/api/

callback handler for the validation part

Sometimes people may need to do something else when there is a validation error, like recording the metric. Since the validation happens before the endpoint function, there should be a way to do this.

[BUG] Authorization header not usable in Swagger UI

Describe the bug
When defining a custom header model with a field Authorization,

class RequestHeader(BaseModel):
    Authorization: str

and using it as such:

@api_validator.validate(headers=RequestHeader)
def query():
    ...

the rendered openapi.json contains Authorization as a parameter:

"parameters": [
          {
            "in": "header", 
            "name": "Authorization", 
            "required": true, 
            "type": "string"
          }
        ], 

which is unusable, because it is restricted by swagger-ui per https://swagger.io/docs/specification/describing-parameters/#header-parameters: "Note: Header parameters named Accept, Content-Type and Authorization are not allowed"

To Reproduce
See pydantic model above. To server the swagger UI, I'm using flask-swagger-ui.

Expected behavior
I'd expect the restricted headers (Accept, Content-Type and Authorization) to be formatted not as parameters, but as their corresponding OpenAPI keywords per https://swagger.io/docs/specification/describing-parameters/#header-parameters

Error Message
No error message from SpecTree, but in Swagger UI, the Authorization header is NOT sent in the request when executing the request.

Desktop (please complete the following information):

  • OS: macOS
  • Version: Catalina

Python Information (please complete the following information):

  • Python Version: 3.7.2
  • Library Version spectree=v0.3.3
  • Other dependencies: flask-swagger-ui==3.25.0

Let me know if you need more information. Awesome project btw!

headers

Users may need to define their customized headers contain some required parameters.

For example, Authorization and Cookie.

[FEATURE] objects passed into validate() to be accessible in endpoint

Coming from flask_pydantic library, spectree has similar signature using validate() function and providing models for validation.

However, in flask_pydantic you get access to your model directly and in spectree it gets converted to dictionary from an object. It would be nice to persist that object.

Example:

@api_spec.validate(json=BodyModel, resp=Response(HTTP_200=ResponseModel), tags=["push"])
def push(account: str):
    # request.json is a dictionary
    # would like access to BodyModel with request data in here

[BUG] swagger 2.0 errors

Describe the bug

I'm using this library to generate swagger:2.0 spec and ran into few issues with the generated spec via .spec attribute.

  1. editor.swagger.io expects swagger: '2.0' for the version instead of the generated openapi: '2.0'
  2. under paths, the correct key is operationId but generated version has operationID which errors out as invalid
should NOT have additional properties
additionalProperty: operationID

To Reproduce
Steps to reproduce the behavior:

SpecTree('flask', title="API", path="docs", openapi_version="2.0").spec

Expected behavior
a valid swagger data for 2.0 version

Python Information (please complete the following information):

  • Python Version [ Python=3.6]
    spectree==0.3.6

Additional context
I'm happy to submit a PR if allowed to fix above issues as long as the maintainer agrees with above issues.

[BUG] 'api.validate' doesn't work on GET endpoints

Describe the bug
@api.validate doesn't work on GET requests.

To Reproduce
The sample code in https://github.com/0b01001001/spectree/blob/master/examples/falcon_demo.py#L56

    @api.validate(tags=[demo])
    def on_get(self, req, resp):

Expected behavior
I expect at least It supports validation over query but not for the request body as GET doesn't have any request body.

Error Message

{
  "title": "Invalid JSON",
  "description": "Could not parse an empty JSON body"
}

Desktop (please complete the following information):

  • OS: Linux
  • Version ArchLinux

Python Information (please complete the following information):

  • Python Version Python=3.9.5
  • Library Version spectree=v0.5.0
  • Other dependencies falcon=3.0.1

Additional context

[BUG] Incorrect spec definition produced when using flask's `any` converter

The flask plugin currently produces the following spec for a route parameter that uses the any converter:

if converter == "any":
    schema = {
        "type": "array",
        "items": {
            "type": "string",
            "enum": args,
        },
    }

As seen here -> https://github.com/0b01001001/spectree/blob/master/spectree/plugins/flask_plugin.py#L80

However the any converter behaviour is defined as the following:

Matches one of the items provided

As seen here -> https://github.com/pallets/werkzeug/blob/8be9899df5cf8a930d961fc5295001744373da0b/src/werkzeug/routing.py#L1227

As such the correct spec produced should actually be:

if converter == "any":
    schema = {
        "type": "string",
        "enum": args,
    }

Any objection if I create a PR to fix this?

[BUG]description for query paramters can not show in swagger ui

Hi, when I add a description for a schema used in query, it can not show in swagger ui but can show in Redoc

@HELLO.route('/', methods=['GET'])
@api.validate(query=HelloForm)
def hello():
    """
    hello 注释
    :return:
    """
   return 'ok'

class HelloForm(BaseModel):
    """
    hello表单
    """
    user: str # 用户名称
    msg: str = Field(description='msg test', example='aa')
    index: int
    data: HelloGetListForm
    list: List[HelloListForm]

截屏2020-10-12 下午7 54 52
截屏2020-10-12 下午7 53 59

[New Feature] Support securitySchemes

I am using JWT token in OpenAPI.
I understand that securityschemes defines security schemes. Please consider adding securityschemes in Components object

https://github.com/0b01001001/spectree/blob/v0.4.2/spectree/spec.py#L236

spec = {
            "openapi": self.config.OPENAPI_VERSION,
            "info": {
                "title": self.config.TITLE,
                "version": self.config.VERSION,
                "description": self.config.DESCRIPTION,
            },
            "tags": list(tags.values()),
            "paths": {**routes},
            "components": {"schemas": {**self.models, **self._get_model_definitions()}},
        }

just like:

components:
  schemas:
     ...
  securitySchemes:
    api_key:
      type: apiKey
      name: api_key
      in: header
    jwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
}

Standard Starlette static mount breaks openapi.json endpoint [BUG]

Describe the bug
Adding a static mount to the routes will result in failure to load openapi.json, with an internal server error.

To Reproduce
Steps to reproduce the behavior:

Given the provided Starlette example (with the needed fix to the api mount path, as it is incorrectly provided in the example):

  • Add a static mount to the routes:
...
from starlette.staticfiles import StaticFiles
...
    app = Starlette(routes=[
        Mount('/api', routes=[
            Route('/user', user_profile, methods=['POST']),
        ]),
        Mount('/static', StaticFiles(directory="static"), name='static'),
    ])
...
  • navigate to apidoc/openapi.json

Expected behavior
Should be able to load openapi.json, redoc, and swagger, even if a static mount location is specified.

Error Message

Traceback (most recent call last):
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/uvicorn/protocols/http/h11_impl.py", line 389, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/middleware/errors.py", line 181, in __call__
    raise exc from None
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/middleware/errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/exceptions.py", line 82, in __call__
    raise exc from None
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/routing.py", line 566, in __call__
    await route.handle(scope, receive, send)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/routing.py", line 227, in handle
    await self.app(scope, receive, send)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/routing.py", line 43, in app
    response = await run_in_threadpool(func, request)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/starlette/concurrency.py", line 34, in run_in_threadpool
    return await loop.run_in_executor(None, func, *args)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/spectree/plugins/starlette_plugin.py", line 30, in <lambda>
    lambda request: JSONResponse(self.spectree.spec),
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/spectree/spec.py", line 60, in spec
    self._spec = self._generate_spec()
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/spectree/spec.py", line 150, in _generate_spec
    for route in self.backend.find_routes():
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/spectree/plugins/starlette_plugin.py", line 129, in find_routes
    parse_route(self.app)
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/spectree/plugins/starlette_plugin.py", line 127, in parse_route
    parse_route(route, prefix=f'{prefix}{route.path}')
  File "/Users/scott/.pyenv/versions/3.7.7/lib/python3.7/site-packages/spectree/plugins/starlette_plugin.py", line 102, in parse_route
    for route in app.routes:
TypeError: 'NoneType' object is not iterable

Desktop (please complete the following information):

  • OS: Mac
  • Version Catalina 10.15.5

Python Information (please complete the following information):

  • Python Version 3.7.7
  • Library Version spectree==0.3.8
  • Other dependencies starlette==0.13.8

Additional context
n/a

[BUG] query works error

Describe the bug
query always fails validation.

To Reproduce
Steps to reproduce the behavior:

from flask import Flask, request, jsonify
from pydantic import BaseModel
from spectree import SpecTree, Response

class Q(BaseModel):
    q: str


app = Flask(__name__)
api = SpecTree('flask')


@app.route('/api/user', methods=['GET'])
@api.validate(query=Q, resp=Response(HTTP_200=Q, HTTP_403=None), tags=['api'])
def user_profile():
    print(request.context.query)
    return jsonify(q='it works')


if __name__ == "__main__":
    api.register(app)
    app.run(port=8000)

Expected behavior

[
  {
    "loc": [
      "q"
    ],
    "msg": "str type expected",
    "type": "type_error.str"
  }
]

Error Message
image

Desktop (please complete the following information):

  • OS [Linux]
  • Version [Debian 10]

Python Information (please complete the following information):

  • Python Version [ Python=3.6.9]
  • Library Version [ spectree=0.3.13]
  • Other dependencies [flask=1.1.2]

[Feature] Allow for extra keywords in a routes' parameter specifications

The specification keywords for routes are currently hardcoded to only generate name, in, schema, required, description as is dictated here, specifically this block of code:

params.append(
    {
        "name": name,
        "in": "query",
        "schema": schema,
        "required": name in query.get("required", []),
        "description": schema.get("description", ""),
    }
)

However there are other keywords that might want to be added for different routes and models. For example the style and explode keywords as explained here. It would be beneficial to have some way for this to be passed in on a model-by-model basis for different routes. Something akin to the following:

params.append(
    {
        "name": name,
        "in": "query",
        "schema": schema,
        "required": name in query.get("required", []),
        "description": schema.get("description", ""),
        **extra_spec_kwargs
    }
)

Although I'm not sure what the best way to provide extra_spec_kwargs would be with the current API.

consistent responses and exceptions

Currently, starlette will return its own starlette.Response like JSONResponse, while flask and falcon return an inherit instance of pydantic.BaseModel. To reduce users' work, all of these should return the frameworks' own type.

On the other hand, exceptions are not caught by spectree, so it won't verify if it's the correct format as defined.

[BUG] When using Flask MethodViews, request and response schemas aren't associated with the api paths

Describe the bug
When using flask method views, even though the request and response schemas are generated in the open api spec, they are not associated with api path. So the paths field in the openapi json is this for the example below -

    "paths": {
        "/api/v1/hello": {
            "post": {
                "description": "",
                "operationId": "post_/api/v1/hello",
                "parameters": [],
                "responses": {},
                "summary": "Hello <POST>",
                "tags": []
            }
        }
    }

To Reproduce
You can run the following python code and check the generated openapi spec.

from flask import Flask
from flask.views import MethodView
from spectree import SpecTree, Response
from pydantic import BaseModel

app = Flask(__name__)
spectree = SpecTree('flask')


class HelloResponse(BaseModel):
    hello: str
    status: int


class HelloRequest(BaseModel):
    test: int


class Hello(MethodView):
    @spectree.validate(json=HelloRequest, resp=Response(HTTP_200=HelloResponse))
    def post():
        return {'hello': 'world', 'status': 200}


app.add_url_rule('/api/v1/hello', view_func=Hello.as_view('Hello'))
spectree.register(app)


if __name__ == '__main__':
    app.run(port=8081)

You can see the generated openapi spec here - openapi spec

Expected behavior
I expected that the generated request and response schema would be associated with the /api/v1/hello path instead of it being blank.

Error Message
There's no error message as such.

Desktop (please complete the following information):

  • OS: Linux
  • Version: Ubuntu 20.04

Python Information (please complete the following information):

  • Python Version - 3.8.3
  • Library Version spectree=0.3.16
  • Other dependencies Flask=1.0.2

Please let me know if you need any additional information. I'd also be happy to contribute a PR if you could direct me a bit in terms of where in the code the issue might be.

[BUG] Incompatibility with OpenAPI V3 specifications

(First of all, I've opened this PR: #91 in order to fix the bug)

Describe the bug
When generating an openapi schema for V3, we have these warnings that pop up: object instance has properties which are not allowed by the schema: [\"definitions\"]

To Reproduce
Steps to reproduce the behavior:

  • Having a nested model defined

Expected behavior
I'd expect the references to point to #/components/schemas/{model} and not #/definitions/{model}.
Also, for this, the definitions need to be moved to the schemas section.

Error Message
object instance has properties which are not allowed by the schema: [\"definitions\"]

Desktop (please complete the following information):

  • OS: Linux
  • Version Ubuntu-18.04

Python Information (please complete the following information):

  • Python=3.7
  • spectree=0.3.8
  • Other dependencies pydantic=1.7.3

Additional context
Changelog in Pydantic: https://pydantic-docs.helpmanual.io/changelog/#changes (search for ref_template)

Adopt Black for code formatting?

Hello again,
Black is becoming the de facto a standard for formatting Python code. By following the rule "if you can automate it, automate it", this can eliminate some tedious manual formatting, and enforce uniform code style from other potential contributors.

How would you feel about it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.