diff options
Diffstat (limited to 'python-spectree.spec')
| -rw-r--r-- | python-spectree.spec | 1743 |
1 files changed, 1743 insertions, 0 deletions
diff --git a/python-spectree.spec b/python-spectree.spec new file mode 100644 index 0000000..da79b69 --- /dev/null +++ b/python-spectree.spec @@ -0,0 +1,1743 @@ +%global _empty_manifest_terminate_build 0 +Name: python-spectree +Version: 1.1.0 +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/5d/ca/35c39ce7818474ceb1e9d34529f2d7874b166a3a5cd3ff4892c270c3a080/spectree-1.1.0.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-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: + +<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> + +> 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: + +<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> + +> 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: + +<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> + +> 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.0 + +%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 11 2023 Python_Bot <Python_Bot@openeuler.org> - 1.1.0-1 +- Package Spec generated |
