# Flask Pydantic API
A wrapper for flask methods allowing them to use Pydantic argment and response types.
## Features
1. Use pydantic models for request data validation (post bodies and query strings) as well as for formatting responses
2. Type annotation driven on the view function instead of the decorator.
3. OpenAPI schema generation and documentation
4. Smart response fields and expansions using [pydantic-enhanced-serializer](https://github.com/adamsussman/pydantic-enhanced-serializer).
5. Async views
6. Fold path parameters into input Pydantic models
## Installation
```console
$ pip install flask-pydantic-api
```
With support for [pydantic-enhanced-serializer](https://github.com/adamsussman/pydantic-enhanced-serializer):
```console
$ pip install flask-pydantic-api[serializer]
```
## Basic Usage
```python
from flask import Flask
from flask_pydantic_api import pydantic_api
from pydantic import BaseModel
app = Flask("my_app")
class RequestBody(BaseModel):
field1: str
field2: Optional[int]
class ResponseBody(BaseModel):
response_field1: str
# GET with query string field1=...&field2=..., responding with json RequestBody
@app.get("/api/something")
@pydantic_api(
name="Go get something", # Name of path operation in OpenAPI schema
tags=["MyTag"], # OpenAPI tags
)
def do_work(body: RequestBody) -> ResponseBody:
return ResponseBody(....)
# POST with body
@app.post("/api/something_else")
@pydantic_api(
name="Go do something", # Name of path operation in OpenAPI schema
tags=["MyTag"], # OpenAPI tags
)
def do_work_post(body: RequestBody) -> ResponseBody:
return ResponseBody(....)
```
## OpenAPI
This library will generate the openapi.json schema to go with your usage of `@pydantic_api`. An example
view is provided to serve it using [RapiDoc](https://rapidocweb.com/), but you can use any other openapi
viewer you wish.
```python
from flask_pydantic_api import apidocs_views
app = Flask("my_app")
# GET /apidocs will render the rapidoc viewer
# GET /apidocs/openapi.json will render the OpenAPI schema
app.register_blueprint(apidocs_views.blueprint, url_prefix="/apidocs")
```
Note that you may wish to customize your schema results more than this module provides. In that case:
```python
from flask_pydantic_api.openapi import get_openapi_schema
@app.get("/path/openapi.json")
def get_openapi_schema() -> str:
# param Info: from openapi_schema_pydantic
# returns: openapi_schema_pydantic.OpenAPI
my_schema = get_openapi_schema(info)
# customize my_schema as wanted...
return make_response(
(
my_schema.json(by_alias=True, exclude_none=True, indent=2),
{"content-type": "application/json"},
)
)
```
## Configuration and Parameters
`@pydantic_api` accepts the following parameters:
* `name`: str - Name for this operation that will be used in the OpenAPI schema
* `Tags`: List[str] - Tags that will be used for this operation in the OpenAPI schema
* `success_status_code`: int = 200 - HTTP Status code that will be used on successful response
* `merge_path_parameters`: bool = False - See [Path Parameter Folding](#patharguments)
* `request_fields_name`: str = "fields" - If using `pydantic-enhanced-serialzer` this is the name of the request parameter that controls the fieldsets returned. See [Using the Enhanced Serializer](#serializer).
* `maximum_expansion_depth`: int = 5 - If using `pydantic-enhanced-serialzer` this controls how deep expansions can go. See [Using the Enhanced Serializer](#serializer).
Flask configuration:
* `FLASK_PYDANTIC_API_RENDER_ERRORS`: bool = True. If true, pydantic validation errors will be rendered to json and returned as a normal response. If false, pydantic errors will yield a standard ValidationError exception.
* `FLASK_PYDANTIC_API_ERROR_STATUS_CODE`: int = 400. If `FLASK_PYDANTIC_API_RENDER_ERRORS` is true, this is the HTTP status code that will be returned.
<a name="patharguments"></a>
## Path Parameter Folding
For paths that include parameters, you can request that the path parameters be moved into the pydantic
object for the request body. In this case you will no longer need the parameter as an argument to
your view function.
* Use the `merge_path_parameters` argument to `@pydantic_api` to control this.
* For this to work, a field of the same name must exist in the request body model
```python
# Normally...
class RequestBodyNormal(BaseModel):
field1: str
@app.post("/path/<path_param1>/whatever")
@pydantic_api()
def do_work(path_param1: str, body: RequestBody) -> Response:
path_param1 = "whatever was in path"
...
```
```python
# With merging:
class RequestBodyNormal(BaseModel):
path_param1: str # path_param1 is now here INSTEAD of the do_work signature
field1: str
@app.post("/path/<path_param1>/whatever")
@pydantic_api(merge_path_parameters=True)
def do_work(body: RequestBody) -> Response:
body.path_param1 # use this instead of the function arg
...
```
## Response Object Flexibility
When returning from an api view, you will typically instantiate a populated response model and return that.
You can also return a dict, which will be cast into the response model.
You can also return any other object that Flask can handle.
```python
class MyResponseModel(BaseModel):
field1: str
field2: int
# returning a model instance
@app.get("/")
@pydantic_api()
def do_work() -> MyResponseModel:
...
model = MyResponseModel(field1="foo", field2=1234)
return model
# Returning a dict that is expected to be compliant with MyResponseModel:
# To make mypy happy, you need to indicate a dict return, but for the
# OpenAPI schema to work, you also need to specify the model. Make
# both happy with a Union return type.
#
# NOTE: if the dict fails validation with MyResponseModel, the result
# will be a 500 server error
@app.get("/")
@pydantic_api()
def do_work() -> Union[dict, MyResponseModel]:
...
return {
"field1": "foo",
"field2": 1234,
}
# Return something that isn't a dict or a model.
# What you get here depends on how Flask supports what you are returning.
# If it isn't a dict or a model, @pydantic_api will just pass it through.
@app.get("/")
@pydantic_api()
def do_work() -> SomthingElse:
...
return SomethingElse()
```
## Error Handling
By default, errors on pydantic validations of inputs will return a 400 HTTP status
code with a json response body that encodes the pydantic errors in its native format
(loc, msg, etc).
You can return a status code other than 400 by setting the flask config
`FLASK_PYDANTIC_API_ERROR_STATUS_CODE`.
If you want to handle the error differently (for example to customize the data structure
of the errors), you can turn off the automatic error handling by settings the
flask config `FLASK_PYDANTIC_API_RENDER_ERRORS` to `False`.
When error handling is turned off, pydantic validation errors will throw the
`pydantic.ValidationError` exception. You will need to handle that exception
or else the server response will be a 500 server error. See [Flask Registering
Error Handlers](https://flask.palletsprojects.com/en/2.2.x/errorhandling/#registering).
**Response Validation Errors:**
If pydantic validation fails on your response object, the error will never be serialized
and returned in the response. This is because the client user cannot easily distinguish
between the error happening on input or on your response. Response validation errors will
throw an exception and yield a 500 server error.
<a name="serializer"></a>
## Using the Enhanced Serializer
This module supports [pydantic-enhanced-serializer](https://github.com/adamsussman/pydantic-enhanced-serializer).
It will use it automatically if installed.
The argument parameter used to select fields and expansions is
`fields`. This can be customized with the `request_fields_name`
parameter of `@pydantic_api`. You do not need to specify the `fields`
parameter in your function arguments or request body model.
The `fields` parameter may be in the query string or in the post body. It can
be a list of strings or a string of field names separated by commas.
The maxium expansion depth defaults to 5 and can be controlled with
the `maximum_expansion_depth` parameter of `@pydantic_api`
Example:
```python
class MyResponse(BaseModel):
field1: str
field2: str
class Config:
fieldsets = [
default: ["field2"],
]
@app.get("/something")
@pydantic_api()
def get_something() -> MyResponse:
return MyResponse(field1="value1", field2="value2")
```
```console
curl http://localhost:8080/something?fields=field1,field2
curl http://localhost:8080/something?fields=field1&fields=field2
curl -X POST \
-H'Content-Type: application/json' \
-d '{"fields": ["field1", "field2"]} \
http://localhost:8080/something
```
See [Pydantic Enhanced Serializer](https://github.com/adamsussman/pydantic-enhanced-serializer)
for more information.
## License
This project is licensed under the terms of the MIT license.
Raw data
{
"_id": null,
"home_page": "https://github.com/adamsussman/flask-pydantic-api",
"name": "flask-pydantic-api",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "",
"author": "Adam Sussman",
"author_email": "adam.sussman@gmail.com",
"download_url": "",
"platform": null,
"description": "# Flask Pydantic API\n\nA wrapper for flask methods allowing them to use Pydantic argment and response types.\n\n## Features\n\n1. Use pydantic models for request data validation (post bodies and query strings) as well as for formatting responses\n2. Type annotation driven on the view function instead of the decorator.\n3. OpenAPI schema generation and documentation\n4. Smart response fields and expansions using [pydantic-enhanced-serializer](https://github.com/adamsussman/pydantic-enhanced-serializer).\n5. Async views\n6. Fold path parameters into input Pydantic models\n\n## Installation\n\n```console\n$ pip install flask-pydantic-api\n```\n\nWith support for [pydantic-enhanced-serializer](https://github.com/adamsussman/pydantic-enhanced-serializer):\n\n```console\n$ pip install flask-pydantic-api[serializer]\n```\n\n## Basic Usage\n\n```python\n\n from flask import Flask\n from flask_pydantic_api import pydantic_api\n from pydantic import BaseModel\n\n app = Flask(\"my_app\")\n\n\n class RequestBody(BaseModel):\n field1: str\n field2: Optional[int]\n\n\n class ResponseBody(BaseModel):\n response_field1: str\n\n\n # GET with query string field1=...&field2=..., responding with json RequestBody\n @app.get(\"/api/something\")\n @pydantic_api(\n name=\"Go get something\", # Name of path operation in OpenAPI schema\n tags=[\"MyTag\"], # OpenAPI tags\n )\n def do_work(body: RequestBody) -> ResponseBody:\n return ResponseBody(....)\n\n\n # POST with body\n @app.post(\"/api/something_else\")\n @pydantic_api(\n name=\"Go do something\", # Name of path operation in OpenAPI schema\n tags=[\"MyTag\"], # OpenAPI tags\n )\n def do_work_post(body: RequestBody) -> ResponseBody:\n return ResponseBody(....)\n```\n\n## OpenAPI\n\nThis library will generate the openapi.json schema to go with your usage of `@pydantic_api`. An example\nview is provided to serve it using [RapiDoc](https://rapidocweb.com/), but you can use any other openapi\nviewer you wish.\n\n```python\n\n from flask_pydantic_api import apidocs_views\n\n app = Flask(\"my_app\")\n\n # GET /apidocs will render the rapidoc viewer\n # GET /apidocs/openapi.json will render the OpenAPI schema\n app.register_blueprint(apidocs_views.blueprint, url_prefix=\"/apidocs\")\n```\n\nNote that you may wish to customize your schema results more than this module provides. In that case:\n\n```python\n\n from flask_pydantic_api.openapi import get_openapi_schema\n\n @app.get(\"/path/openapi.json\")\n def get_openapi_schema() -> str:\n # param Info: from openapi_schema_pydantic\n # returns: openapi_schema_pydantic.OpenAPI\n my_schema = get_openapi_schema(info)\n\n # customize my_schema as wanted...\n\n return make_response(\n (\n my_schema.json(by_alias=True, exclude_none=True, indent=2),\n {\"content-type\": \"application/json\"},\n )\n )\n```\n\n## Configuration and Parameters\n\n`@pydantic_api` accepts the following parameters:\n\n* `name`: str - Name for this operation that will be used in the OpenAPI schema\n* `Tags`: List[str] - Tags that will be used for this operation in the OpenAPI schema\n* `success_status_code`: int = 200 - HTTP Status code that will be used on successful response\n* `merge_path_parameters`: bool = False - See [Path Parameter Folding](#patharguments)\n* `request_fields_name`: str = \"fields\" - If using `pydantic-enhanced-serialzer` this is the name of the request parameter that controls the fieldsets returned. See [Using the Enhanced Serializer](#serializer).\n* `maximum_expansion_depth`: int = 5 - If using `pydantic-enhanced-serialzer` this controls how deep expansions can go. See [Using the Enhanced Serializer](#serializer).\n\nFlask configuration:\n\n* `FLASK_PYDANTIC_API_RENDER_ERRORS`: bool = True. If true, pydantic validation errors will be rendered to json and returned as a normal response. If false, pydantic errors will yield a standard ValidationError exception.\n* `FLASK_PYDANTIC_API_ERROR_STATUS_CODE`: int = 400. If `FLASK_PYDANTIC_API_RENDER_ERRORS` is true, this is the HTTP status code that will be returned.\n\n<a name=\"patharguments\"></a>\n## Path Parameter Folding\n\nFor paths that include parameters, you can request that the path parameters be moved into the pydantic\nobject for the request body. In this case you will no longer need the parameter as an argument to\nyour view function.\n\n* Use the `merge_path_parameters` argument to `@pydantic_api` to control this.\n* For this to work, a field of the same name must exist in the request body model\n\n```python\n # Normally...\n class RequestBodyNormal(BaseModel):\n field1: str\n\n @app.post(\"/path/<path_param1>/whatever\")\n @pydantic_api()\n def do_work(path_param1: str, body: RequestBody) -> Response:\n path_param1 = \"whatever was in path\"\n ...\n```\n\n```python\n # With merging:\n class RequestBodyNormal(BaseModel):\n path_param1: str # path_param1 is now here INSTEAD of the do_work signature\n field1: str\n\n @app.post(\"/path/<path_param1>/whatever\")\n @pydantic_api(merge_path_parameters=True)\n def do_work(body: RequestBody) -> Response:\n body.path_param1 # use this instead of the function arg\n ...\n```\n\n## Response Object Flexibility\n\nWhen returning from an api view, you will typically instantiate a populated response model and return that.\n\nYou can also return a dict, which will be cast into the response model.\n\nYou can also return any other object that Flask can handle.\n\n```python\n\n class MyResponseModel(BaseModel):\n field1: str\n field2: int\n\n # returning a model instance\n @app.get(\"/\")\n @pydantic_api()\n def do_work() -> MyResponseModel:\n ...\n model = MyResponseModel(field1=\"foo\", field2=1234)\n return model\n\n # Returning a dict that is expected to be compliant with MyResponseModel:\n # To make mypy happy, you need to indicate a dict return, but for the\n # OpenAPI schema to work, you also need to specify the model. Make\n # both happy with a Union return type.\n #\n # NOTE: if the dict fails validation with MyResponseModel, the result\n # will be a 500 server error\n @app.get(\"/\")\n @pydantic_api()\n def do_work() -> Union[dict, MyResponseModel]:\n ...\n return {\n \"field1\": \"foo\",\n \"field2\": 1234,\n }\n\n # Return something that isn't a dict or a model.\n # What you get here depends on how Flask supports what you are returning.\n # If it isn't a dict or a model, @pydantic_api will just pass it through.\n @app.get(\"/\")\n @pydantic_api()\n def do_work() -> SomthingElse:\n ...\n return SomethingElse()\n```\n\n\n## Error Handling\n\nBy default, errors on pydantic validations of inputs will return a 400 HTTP status\ncode with a json response body that encodes the pydantic errors in its native format\n(loc, msg, etc).\nYou can return a status code other than 400 by setting the flask config\n`FLASK_PYDANTIC_API_ERROR_STATUS_CODE`.\n\nIf you want to handle the error differently (for example to customize the data structure\nof the errors), you can turn off the automatic error handling by settings the\nflask config `FLASK_PYDANTIC_API_RENDER_ERRORS` to `False`.\n\nWhen error handling is turned off, pydantic validation errors will throw the\n`pydantic.ValidationError` exception. You will need to handle that exception\nor else the server response will be a 500 server error. See [Flask Registering\nError Handlers](https://flask.palletsprojects.com/en/2.2.x/errorhandling/#registering).\n\n**Response Validation Errors:**\n\nIf pydantic validation fails on your response object, the error will never be serialized\nand returned in the response. This is because the client user cannot easily distinguish\nbetween the error happening on input or on your response. Response validation errors will\nthrow an exception and yield a 500 server error.\n\n<a name=\"serializer\"></a>\n## Using the Enhanced Serializer\n\nThis module supports [pydantic-enhanced-serializer](https://github.com/adamsussman/pydantic-enhanced-serializer).\nIt will use it automatically if installed.\n\nThe argument parameter used to select fields and expansions is\n`fields`. This can be customized with the `request_fields_name`\nparameter of `@pydantic_api`. You do not need to specify the `fields`\nparameter in your function arguments or request body model.\n\nThe `fields` parameter may be in the query string or in the post body. It can\nbe a list of strings or a string of field names separated by commas.\n\nThe maxium expansion depth defaults to 5 and can be controlled with\nthe `maximum_expansion_depth` parameter of `@pydantic_api`\n\nExample:\n\n```python\n\n class MyResponse(BaseModel):\n field1: str\n field2: str\n\n class Config:\n fieldsets = [\n default: [\"field2\"],\n ]\n\n @app.get(\"/something\")\n @pydantic_api()\n def get_something() -> MyResponse:\n return MyResponse(field1=\"value1\", field2=\"value2\")\n```\n\n```console\n\n curl http://localhost:8080/something?fields=field1,field2\n curl http://localhost:8080/something?fields=field1&fields=field2\n\n curl -X POST \\\n -H'Content-Type: application/json' \\\n -d '{\"fields\": [\"field1\", \"field2\"]} \\\n http://localhost:8080/something\n\n```\n\nSee [Pydantic Enhanced Serializer](https://github.com/adamsussman/pydantic-enhanced-serializer)\nfor more information.\n\n\n## License\n\nThis project is licensed under the terms of the MIT license.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Pydantic based API support for Flask",
"version": "0.9.0",
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "64591d68e501c5d07d5331edeab2218db8fd9a88f51ccdea2678d8e0c72ed792",
"md5": "400dcfae8b8aa3379f852f8a3b41f60b",
"sha256": "f0cffb5e2976f89e53e5bdc96cef8836403b78a9584acb972c1159e59fde5510"
},
"downloads": -1,
"filename": "flask_pydantic_api-0.9.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "400dcfae8b8aa3379f852f8a3b41f60b",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 11227,
"upload_time": "2023-03-19T03:16:39",
"upload_time_iso_8601": "2023-03-19T03:16:39.956604Z",
"url": "https://files.pythonhosted.org/packages/64/59/1d68e501c5d07d5331edeab2218db8fd9a88f51ccdea2678d8e0c72ed792/flask_pydantic_api-0.9.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-03-19 03:16:39",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "adamsussman",
"github_project": "flask-pydantic-api",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "flask-pydantic-api"
}