All Projects → 0b01001001 → spectree

0b01001001 / spectree

Licence: Apache-2.0 license
API spec validator and OpenAPI document generator for Python web frameworks.

Programming Languages

python
139335 projects - #7 most used programming language
Makefile
30231 projects

Projects that are alternatives of or similar to spectree

starlite
Light, Flexible and Extensible ASGI API framework
Stars: ✭ 1,525 (+702.63%)
Mutual labels:  openapi, asgi, redoc, starlette, pydantic
Fastapi
FastAPI framework, high performance, easy to learn, fast to code, ready for production
Stars: ✭ 39,588 (+20735.79%)
Mutual labels:  openapi, redoc, starlette, pydantic
mongox
Familiar async Python MongoDB ODM
Stars: ✭ 113 (-40.53%)
Mutual labels:  asgi, starlette, pydantic
pait
Python Modern API Tools, fast to code
Stars: ✭ 24 (-87.37%)
Mutual labels:  redoc, starlette, pydantic
falcon-apispec
apispec plugin that generates OpenAPI specification (aka Swagger Docs) for Falcon web applications.
Stars: ✭ 44 (-76.84%)
Mutual labels:  openapi, falcon, apispec
asymmetric
Ridiculously fast and easy module-to-API transformations. Learn in minutes, implement in seconds. Batteries included.
Stars: ✭ 35 (-81.58%)
Mutual labels:  openapi, redoc, starlette
Http Router
🎉 Release 2.0 is released! Very fast HTTP router for PHP 7.1+ (incl. PHP8 with attributes) based on PSR-7 and PSR-15 with support for annotations and OpenApi (Swagger)
Stars: ✭ 124 (-34.74%)
Mutual labels:  annotation, annotations, openapi
fastapi-azure-auth
Easy and secure implementation of Azure AD for your FastAPI APIs 🔒 B2C, single- and multi-tenant support.
Stars: ✭ 174 (-8.42%)
Mutual labels:  openapi, asgi
symmetric
A powerful tool to enable super fast module-to-API transformations. Learn in minutes, implement in seconds. Batteries included.
Stars: ✭ 65 (-65.79%)
Mutual labels:  openapi, redoc
AnnotationProcessing
✔️ㅤ[ARTICLE] Writing your own Annotation Processors in Android
Stars: ✭ 47 (-75.26%)
Mutual labels:  annotation, annotations
dart sealed
Dart and Flutter sealed class generator and annotations, with match methods and other utilities. There is also super_enum compatible API.
Stars: ✭ 16 (-91.58%)
Mutual labels:  annotation, annotations
openapi-python-client
Generate modern Python clients from OpenAPI
Stars: ✭ 543 (+185.79%)
Mutual labels:  openapi, openapi-document
aptk
A toolkit project to enable you to build annotation processors more easily
Stars: ✭ 28 (-85.26%)
Mutual labels:  annotation, annotations
async-asgi-testclient
A framework-agnostic library for testing ASGI web applications
Stars: ✭ 123 (-35.26%)
Mutual labels:  asgi, starlette
annotate
Create 3D labelled bounding boxes in RViz
Stars: ✭ 104 (-45.26%)
Mutual labels:  annotation, annotations
cakephp-swagger-bake
Automatically generate OpenAPI, Swagger, and Redoc documentation from your existing CakePHP code.
Stars: ✭ 48 (-74.74%)
Mutual labels:  openapi, redoc
morelia server
Server for MoreliaTalk network
Stars: ✭ 25 (-86.84%)
Mutual labels:  starlette, pydantic
apispec-webframeworks
Web framework plugins for apispec (formally in apispec.ext).
Stars: ✭ 25 (-86.84%)
Mutual labels:  openapi, apispec
AnnotationProcessorStarter
Project to set up basics of a Java annotation processor
Stars: ✭ 19 (-90%)
Mutual labels:  annotation, annotations
React Image Annotation
An infinitely customizable image annotation library built on React
Stars: ✭ 203 (+6.84%)
Mutual labels:  annotation, annotations

SpecTree

GitHub Actions pypi versions Language grade: Python Python document

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

Features

  • Less boilerplate code, only annotations, no need for YAML
  • Generate API document with Redoc UI or Swagger UI 😋
  • Validate query, JSON data, response data with pydantic 😉
  • Current support:
    • Flask demo
    • Falcon demo (including ASGI under Falcon 3+)
    • Starlette demo

Quick Start

install with pip: pip install spectree

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
    • query
    • json
    • headers
    • cookies
    • resp
    • tags
    • security
  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

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.body(deprecated in falcon 3.0 and replaced by 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():
    ...

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.

@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__)
api = SpecTree('flask')


@app.route('/api/user', methods=['POST'])
@api.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__":
    api.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
api = SpecTree("flask", annotations=True)


@app.route('/api/user', methods=['POST'])
@api.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


api = SpecTree('falcon')


class UserProfile:
    @api.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.API()
    app.add_route('/api/user', UserProfile())
    api.register(app)

    httpd = simple_server.make_server('localhost', 8000, app)
    httpd.serve_forever()

Falcon with type annotations

# opt in into annotations feature
api = SpecTree("falcon", annotations=True)


class UserProfile:
    @api.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


api = SpecTree('starlette')


@api.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']),
        ])
    ])
    api.register(app)

    uvicorn.run(app)

Starlette example with type annotations

# opt in into annotations feature
api = SpecTree("flask", annotations=True)


@api.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.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].