Metadata-Version: 2.4
Name: spectree
Version: 3.0.0a0
Summary: Generate OpenAPI document and validate request & response with Python annotations.
Author-Email: Keming Yang <kemingy94@gmail.com>
License-Expression: Apache-2.0
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Free Threading
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Project-URL: Homepage, https://github.com/0b01001001/spectree
Project-URL: documentation, https://0b01001001.github.io/spectree/
Project-URL: repository, https://github.com/0b01001001/spectree
Project-URL: changelog, https://github.com/0b01001001/spectree/releases
Requires-Python: >=3.10
Provides-Extra: pydantic
Requires-Dist: pydantic<3,>=2.11; extra == "pydantic"
Provides-Extra: msgspec
Requires-Dist: msgspec<1,>=0.21; extra == "msgspec"
Provides-Extra: offline
Requires-Dist: offapi>=0.1.1; extra == "offline"
Provides-Extra: falcon
Requires-Dist: falcon>=3; extra == "falcon"
Provides-Extra: starlette
Requires-Dist: starlette[full]>=0.16; extra == "starlette"
Provides-Extra: flask
Requires-Dist: flask>=2; extra == "flask"
Provides-Extra: quart
Requires-Dist: quart>=0.16; extra == "quart"
Description-Content-Type: text/markdown

# SpecTree


[![GitHub Actions](https://github.com/0b01001001/spectree/workflows/Python%20package/badge.svg)](https://github.com/0b01001001/spectree/actions)
[![pypi](https://img.shields.io/pypi/v/spectree.svg)](https://pypi.python.org/pypi/spectree)
[![versions](https://img.shields.io/pypi/pyversions/spectree.svg)](https://github.com/0b01001001/spectree)
[![CodeQL](https://github.com/0b01001001/spectree/actions/workflows/codeql.yml/badge.svg)](https://github.com/0b01001001/spectree/actions/workflows/codeql.yml)
[![Python document](https://github.com/0b01001001/spectree/workflows/Python%20document/badge.svg)](https://0b01001001.github.io/spectree/)

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](https://github.com/kemingy/defspec/).

## Features

* Less boilerplate code, only annotations, no need for YAML :sparkles:
* Generate API document with [Redoc UI](https://github.com/Redocly/redoc), [Scalar UI](https://github.com/scalar/scalar) or [Swagger UI](https://github.com/swagger-api/swagger-ui) :yum:
* Validate query, JSON data, and response data with supported model backends :wink:
  * [pydantic](https://github.com/pydantic/pydantic)
  * [msgspec](https://github.com/jcrist/msgspec)
  * others if you implement the [model adapter protocol](spectree/model_adapter/protocol.py)
* Current support:
  * Flask [demo](#flask)
  * Quart [demo](#quart)
  * Falcon [demo](#falcon)
  * Starlette [demo](#starlette)

## Quick Start

Install with pip:

```bash
pip install "spectree[pydantic]"
```

If you want to install with offline OpenAPI web pages support:

> Offline mode doesn't support SwaggerUI OAuth2 redirection.

```bash
pip install "spectree[pydantic,offline]"
```

If you want to use `msgspec` instead of `pydantic`:

```bash
pip install "spectree[msgspec]"
```

### Examples

Check the [examples](examples) folder.

* [flask example](examples/flask_demo.py)
* [quart example](examples/quart_demo.py)
* [falcon example with logging when validation failed](examples/falcon_demo.py)
* [falcon example with msgspec models](examples/falcon_msgspec_demo.py)
* [starlette example](examples/starlette_demo.py)

### Step by Step

1. Define your data structures for `query`, `json`, `headers`, `cookies`, and `resp` with the model backend you configured for `SpecTree`
2. create `spectree.SpecTree` instance with the web framework name you are using, like `api = SpecTree('flask')`. `SpecTree` uses the `pydantic` adapter by default, or you can pass another adapter explicitly with `model_adapter=...`.
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 from the function annotations (see the examples below). Of course, you can still access them from the original place where the framework offered.
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 your model backend's field metadata and schema documentation for how to attach descriptions.

> Any config I can change?

Of course. Check the [config](https://spectree.readthedocs.io/en/latest/config.html) document.

You can update the config when init the spectree like:

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

> How do I choose between `pydantic` and `msgspec`?

Install the extra for the backend you want to use, then pass the adapter when creating `SpecTree`:

```py
from spectree.model_adapter import get_msgspec_model_adapter

SpecTree("falcon", model_adapter=get_msgspec_model_adapter())
SpecTree("flask")  # uses pydantic by default
```

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

```py
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 can I skip the validation?

Add `skip_validation=True` to the decorator.

Before v1.3.0, this only skip the response validation.

Starts from v1.3.0, this will skip all the validations. As an result, you won't be able to access the validated data from `context`.

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

> What is the callback signature for `before` and `after` hooks?

Both hooks now receive the active model adapter as their last argument:

```py
def before(req, resp, req_validation_error, instance, model_adapter):
    ...


def after(req, resp, resp_validation_error, instance, model_adapter):
    ...
```

This is useful when you need adapter-specific error details or other model-backend behavior in a custom hook.

> How can I use the validation without the OpenAPI document?

The OpenAPI endpoints are added by `spectree.register(app)`. If you don't want to add the OpenAPI endpoints, you don't need to register it to the application.

> 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:

<details>
<summary>Click to expand the code example:</summary>
<p>

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

</p>
</details>


2. 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:

<details>
<summary>Click to expand the code example:</summary>
<p>

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

</p>
</details>

> 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](examples/falcon_demo.py) 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?

```py
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](https://spectree.readthedocs.io/en/latest) or the [test case](tests/test_plugin_flask.py). 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 return my model directly?

Yes, returning an instance produced by your configured model backend will assume the model is valid and bypass Spectree's validation. Spectree will serialize that instance through the active model adapter.

For starlette you should return a `PydanticResponse`:
```py
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

```py
from flask import Flask, jsonify
from pydantic import BaseModel, Field, ConfigDict

from spectree import Response, SpecTree


class Profile(BaseModel):
    name: str
    age: int = Field(..., gt=0, lt=150, description="user age(Human)")

    model_config = ConfigDict(
        json_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(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')`


if __name__ == "__main__":
    spec.register(app)  # if you don't register in api init step
    app.run(port=8000)
```

### Quart

```py
from pydantic import BaseModel, Field, ConfigDict
from quart import Quart, jsonify

from spectree import Response, SpecTree


class Profile(BaseModel):
    name: str
    age: int = Field(..., gt=0, lt=150, description="user age")

    model_config = ConfigDict(
        json_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(resp=Response(HTTP_200=Message, HTTP_403=None), tags=["api"])
async 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")`


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

### Falcon

```py
from wsgiref import simple_server

import falcon
from pydantic import BaseModel, Field

from spectree import Response, SpecTree


class Profile(BaseModel):
    name: str
    age: int = Field(..., gt=0, lt=150, description="user age(Human)")


class Message(BaseModel):
    text: str


spec = SpecTree("falcon")


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(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()
```

### Starlette

```py
import uvicorn
from pydantic import BaseModel, Field
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Mount, Route

from spectree import Response, SpecTree

# from spectree.plugins.starlette_plugin import PydanticResponse


class Profile(BaseModel):
    name: str
    age: int = Field(..., gt=0, lt=150, description="user age(Human)")


class Message(BaseModel):
    text: str


spec = SpecTree("starlette")


@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(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)
```


## 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.model_validator(mode="before")`](https://docs.pydantic.dev/dev/concepts/validators/#model-validators) 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.
