%global _empty_manifest_terminate_build 0
Name: python-spectree
Version: 1.1.2
Release: 1
Summary: generate OpenAPI document and validate request&response with Python annotations.
License: Apache-2.0
URL: https://github.com/0b01001001/spectree
Source0: https://mirrors.nju.edu.cn/pypi/web/packages/cf/fd/08b851b82185379ea172582c93ca3489dac04f52ca705783ad82c11c033f/spectree-1.1.2.tar.gz
BuildArch: noarch
Requires: python3-pydantic
Requires: python3-pytest
Requires: python3-flake8
Requires: python3-black
Requires: python3-isort
Requires: python3-autoflake
Requires: python3-mypy
Requires: python3-syrupy
Requires: python3-pre-commit
Requires: python3-pydantic[email]
Requires: python3-falcon
Requires: python3-flask
Requires: python3-quart
Requires: python3-starlette[full]
%description
# SpecTree
[](https://github.com/0b01001001/spectree/actions)
[](https://pypi.python.org/pypi/spectree)
[](https://github.com/0b01001001/spectree)
[](https://github.com/0b01001001/spectree/actions/workflows/codeql.yml)
[](https://0b01001001.github.io/spectree/)
Yet another library to generate OpenAPI documents and validate requests & responses with Python annotations.
## Features
* Less boilerplate code, only annotations, no need for YAML :sparkles:
* Generate API document with [Redoc UI](https://github.com/Redocly/redoc) or [Swagger UI](https://github.com/swagger-api/swagger-ui) :yum:
* Validate query, JSON data, response data with [pydantic](https://github.com/samuelcolvin/pydantic/) :wink:
* Current support:
* Flask [demo](#flask)
* Quart [demo](#quart)
* Falcon [demo](#falcon)
* Starlette [demo](#starlette)
## 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](/examples) folder.
* [flask example](/examples/flask_demo.py)
* [falcon example with logging when validation failed](/examples/falcon_demo.py)
* [starlette example](examples/starlette_demo.py)
### 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.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](https://pydantic-docs.helpmanual.io/usage/schema/) document about description in `Field`.
> 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')
```
> 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 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:
```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():
...
```
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:
Click to expand the code example:
```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():
...
```
> 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 skip the validation?
Add `skip_validation=True` to the decorator.
```py
@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`:
```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, 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
```python
# 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
```py
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
```python
# 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
```py
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
```python
# 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
```py
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
```python
# 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)`](https://pydantic-docs.helpmanual.io/usage/validators/#root-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. So I suggest not to use list type in query until I find a suitable way to fix it.
%package -n python3-spectree
Summary: generate OpenAPI document and validate request&response with Python annotations.
Provides: python-spectree
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-pip
%description -n python3-spectree
# SpecTree
[](https://github.com/0b01001001/spectree/actions)
[](https://pypi.python.org/pypi/spectree)
[](https://github.com/0b01001001/spectree)
[](https://github.com/0b01001001/spectree/actions/workflows/codeql.yml)
[](https://0b01001001.github.io/spectree/)
Yet another library to generate OpenAPI documents and validate requests & responses with Python annotations.
## Features
* Less boilerplate code, only annotations, no need for YAML :sparkles:
* Generate API document with [Redoc UI](https://github.com/Redocly/redoc) or [Swagger UI](https://github.com/swagger-api/swagger-ui) :yum:
* Validate query, JSON data, response data with [pydantic](https://github.com/samuelcolvin/pydantic/) :wink:
* Current support:
* Flask [demo](#flask)
* Quart [demo](#quart)
* Falcon [demo](#falcon)
* Starlette [demo](#starlette)
## 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](/examples) folder.
* [flask example](/examples/flask_demo.py)
* [falcon example with logging when validation failed](/examples/falcon_demo.py)
* [starlette example](examples/starlette_demo.py)
### 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.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](https://pydantic-docs.helpmanual.io/usage/schema/) document about description in `Field`.
> 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')
```
> 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 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:
```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():
...
```
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:
Click to expand the code example:
```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():
...
```
> 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 skip the validation?
Add `skip_validation=True` to the decorator.
```py
@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`:
```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, 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
```python
# 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
```py
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
```python
# 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
```py
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
```python
# 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
```py
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
```python
# 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)`](https://pydantic-docs.helpmanual.io/usage/validators/#root-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. So I suggest not to use list type in query until I find a suitable way to fix it.
%package help
Summary: Development documents and examples for spectree
Provides: python3-spectree-doc
%description help
# SpecTree
[](https://github.com/0b01001001/spectree/actions)
[](https://pypi.python.org/pypi/spectree)
[](https://github.com/0b01001001/spectree)
[](https://github.com/0b01001001/spectree/actions/workflows/codeql.yml)
[](https://0b01001001.github.io/spectree/)
Yet another library to generate OpenAPI documents and validate requests & responses with Python annotations.
## Features
* Less boilerplate code, only annotations, no need for YAML :sparkles:
* Generate API document with [Redoc UI](https://github.com/Redocly/redoc) or [Swagger UI](https://github.com/swagger-api/swagger-ui) :yum:
* Validate query, JSON data, response data with [pydantic](https://github.com/samuelcolvin/pydantic/) :wink:
* Current support:
* Flask [demo](#flask)
* Quart [demo](#quart)
* Falcon [demo](#falcon)
* Starlette [demo](#starlette)
## 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](/examples) folder.
* [flask example](/examples/flask_demo.py)
* [falcon example with logging when validation failed](/examples/falcon_demo.py)
* [starlette example](examples/starlette_demo.py)
### 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.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](https://pydantic-docs.helpmanual.io/usage/schema/) document about description in `Field`.
> 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')
```
> 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 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:
```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():
...
```
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:
Click to expand the code example:
```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():
...
```
> 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 skip the validation?
Add `skip_validation=True` to the decorator.
```py
@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`:
```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, 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
```python
# 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
```py
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
```python
# 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
```py
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
```python
# 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
```py
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
```python
# 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)`](https://pydantic-docs.helpmanual.io/usage/validators/#root-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. So I suggest not to use list type in query until I find a suitable way to fix it.
%prep
%autosetup -n spectree-1.1.2
%build
%py3_build
%install
%py3_install
install -d -m755 %{buildroot}/%{_pkgdocdir}
if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi
if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi
if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi
if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi
pushd %{buildroot}
if [ -d usr/lib ]; then
find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/lib64 ]; then
find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/bin ]; then
find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/sbin ]; then
find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst
fi
touch doclist.lst
if [ -d usr/share/man ]; then
find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst
fi
popd
mv %{buildroot}/filelist.lst .
mv %{buildroot}/doclist.lst .
%files -n python3-spectree -f filelist.lst
%dir %{python3_sitelib}/*
%files help -f doclist.lst
%{_docdir}/*
%changelog
* Tue Apr 25 2023 Python_Bot - 1.1.2-1
- Package Spec generated