Skip to content

Commit

Permalink
Merge pull request #13 from adriencaccia/add-kwargs-parsing
Browse files Browse the repository at this point in the history
Add kwargs parsing
  • Loading branch information
bauerji committed Sep 8, 2020
2 parents c45a761 + 5a52fce commit 71df60c
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 87 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# VS Code settings
.vscode
165 changes: 103 additions & 62 deletions README.md
@@ -1,4 +1,5 @@
# Flask-Pydantic

[![Actions Status](https://github.com/bauerji/flask_pydantic/workflows/Python%20package/badge.svg?branch=master)](https://github.com/bauerji/flask_pydantic/actions)
[![PyPI](https://img.shields.io/pypi/v/Flask-Pydantic?color=g)](https://pypi.org/project/Flask-Pydantic/)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/bauerji/flask_pydantic.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/bauerji/flask_pydantic/context:python)
Expand All @@ -8,15 +9,23 @@
Flask extension for integration of the awesome [pydantic package](https://github.com/samuelcolvin/pydantic) with [Flask](https://palletsprojects.com/p/flask/).

## Installation
``python3 -m pip install Flask-Pydantic``

`python3 -m pip install Flask-Pydantic`

## Basics
`validate` decorator validates query and body request parameters and makes them accessible via flask's `request` variable

`validate` decorator validates query and body request parameters and makes them accessible two ways:

1. [Using `validate` arguments, via flask's `request` variable](#basic-example)

| **parameter type** | **`request` attribute name** |
|:--------------:|:------------------------:|
| query | `query_params` |
| body | `body_params` |
| :----------------: | :--------------------------: |
| query | `query_params` |
| body | `body_params` |

2. [Using the decorated function argument parameters type hints](#using-the-decorated-function-kwargs)

---

- Success response status code can be modified via `on_success_status` parameter of `validate` decorator.
- `response_many` parameter set to `True` enables serialization of multiple models (route function should therefore return iterable of models).
Expand All @@ -26,10 +35,13 @@ Flask extension for integration of the awesome [pydantic package](https://github
For more details see in-code docstring or example app.

## Usage

### Basic example
Simply use `validate` decorator on route function.

Simply use `validate` decorator on route function.

:exclamation: Be aware that `@app.route` decorator must precede `@validate` (i. e. `@validate` must be closer to the function declaration).

```python
from typing import Optional

Expand Down Expand Up @@ -69,48 +81,48 @@ def post():
nickname=request.body_params.nickname,
)
```

- `age` query parameter is a required `int`
- if none is provided the response contains:
```json
{
"validation_error": {
"query_params": [
{
"loc": [
"age"
],
"msg": "field required",
"type": "value_error.missing"
}
]
- if none is provided the response contains:
```json
{
"validation_error": {
"query_params": [
{
"loc": ["age"],
"msg": "field required",
"type": "value_error.missing"
}
}
```
- for incompatible type (e. g. string `/?age=not_a_number`)
```json
{
"validation_error": {
"query_params": [
{
"loc": [
"age"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
]
}
}
```
- for incompatible type (e. g. string `/?age=not_a_number`)
```json
{
"validation_error": {
"query_params": [
{
"loc": ["age"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
}
```
]
}
}
```
- likewise for body parameters
- example call with valid parameters:
``curl -XPOST http://localhost:5000/?age=20 --data '{"name": "John Doe"}' -H 'Content-Type: application/json'``
`curl -XPOST http://localhost:5000/?age=20 --data '{"name": "John Doe"}' -H 'Content-Type: application/json'`

-> ``{"id": 2, "age": 20, "name": "John Doe", "nickname": null}``
-> `{"id": 2, "age": 20, "name": "John Doe", "nickname": null}`

### Modify response status code

The default success status code is `200`. It can be modified in two ways

- in return statement

```python
# necessary imports, app and models definition
...
Expand All @@ -125,7 +137,9 @@ def post():
nickname=request.body_params.nickname,
), 201
```

- in `validate` decorator

```python
@app.route("/", methods=["POST"])
@validate(body=BodyModel, query=QueryModel, on_success_status=201)
Expand All @@ -135,41 +149,68 @@ def post():

Status code in case of validation error can be modified using `FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE` flask configuration variable.

### Using the decorated function `kwargs`

Instead of passing `body` and `query` to `validate`, it is possible to directly
defined them by using type hinting in the decorated function.

```python
# necessary imports, app and models definition
...

@app.route("/", methods=["POST"])
@validate()
def post(body: BodyModel, query: QueryModel):
return ResponseModel(
id=id_,
age=query.age,
name=body.name,
nickname=body.nickname,
)
```

This way, the parsed data will be directly available in `body` and `query`.
Furthermore, your IDE will be able to correctly type them.

### Example app

For more complete examples see [example application](https://github.com/bauerji/flask_pydantic/tree/master/example_app).

### Configuration

The behaviour can be configured using flask's application config
``FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE`` - response status code after validation error (defaults to `400`)
`FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE` - response status code after validation error (defaults to `400`)

## Contributing

Feature requests and pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

- clone repository
```bash
git clone https://github.com/bauerji/flask_pydantic.git
cd flask_pydantic
```
- create virtual environment and activate it
```bash
python3 -m venv venv
source venv/bin/activate
```
- install development requirements
```bash
python3 -m pip install -r requirements/test.pip
```
- checkout new branch and make your desired changes (don't forget to update tests)
```bash
git checkout -b <your_branch_name>
```
- run tests
```bash
python3 -m pytest
```
- if tests fails on Black tests, make sure You have your code compliant with style of [Black formatter](https://github.com/psf/black)
- push your changes and create a pull request to master branch
```bash
git clone https://github.com/bauerji/flask_pydantic.git
cd flask_pydantic
```
- create virtual environment and activate it
```bash
python3 -m venv venv
source venv/bin/activate
```
- install development requirements
```bash
python3 -m pip install -r requirements/test.pip
```
- checkout new branch and make your desired changes (don't forget to update tests)
```bash
git checkout -b <your_branch_name>
```
- run tests
```bash
python3 -m pytest
```
- if tests fails on Black tests, make sure You have your code compliant with style of [Black formatter](https://github.com/psf/black)
- push your changes and create a pull request to master branch

## TODOs:

- header request parameters
- cookie request parameters
18 changes: 18 additions & 0 deletions example_app/app.py
Expand Up @@ -53,6 +53,24 @@ def post():
)


@app.route("/kwargs", methods=["POST"])
@validate()
def post(body: BodyModel, query: QueryModel):
"""
Basic example with both query and body parameters, response object serialization.
This time using the decorated function kwargs `body` and `query` type hinting
"""
# save model to DB
id_ = 3

return ResponseModel(
id=id_,
age=query.age,
name=body.name,
nickname=body.nickname,
)


@app.route("/many", methods=["GET"])
@validate(response_many=True)
def get_many():
Expand Down
67 changes: 42 additions & 25 deletions flask_pydantic/core.py
Expand Up @@ -85,39 +85,47 @@ def validate(
- request.query_params
- request.body_params
Or directly as `kwargs`, if you define them in the decorated function.
`exclude_none` whether to remove None fields from response
`response_many` whether content of response consists of many objects
(e. g. List[BaseModel]). Resulting response will be an array of serialized
models.
`request_body_many` whether response body contains array of given model
(request.body_params then contains list of models i. e. List[BaseModel])
example:
example::
from flask import request
from flask_pydantic import validate
from pydantic import BaseModel
from flask import request
from flask_pydantic import validate
from pydantic import BaseModel
class Query(BaseModel):
query: str
class Query(BaseModel):
query: str
class Body(BaseModel):
color: str
class Body(BaseModel):
color: str
class MyModel(BaseModel):
id: int
color: str
description: str
class MyModel(BaseModel):
id: int
color: str
description: str
...
...
@app.route("/")
@validate(query=Query, body=Body)
def test_route():
query = request.query_params.query
color = request.body_params.query
@app.route("/")
@validate(query=Query, body=Body)
def test_route():
query = request.query_params.query
color = request.body_params.query
return MyModel(...)
return MyModel(...)
@app.route("/kwargs")
@validate()
def test_route_kwargs(query:Query, body:Body):
return MyModel(...)
-> that will render JSON response with serialized MyModel instance
"""
Expand All @@ -126,22 +134,26 @@ def decorate(func: Callable[[InputParams], Any]) -> Callable[[InputParams], Any]
@wraps(func)
def wrapper(*args, **kwargs):
q, b, err = None, None, {}
if query:
query_params = convert_query_params(request.args, query)
query_in_kwargs = func.__annotations__.get("query")
query_model = query_in_kwargs or query
if query_model:
query_params = convert_query_params(request.args, query_model)
try:
q = query(**query_params)
q = query_model(**query_params)
except ValidationError as ve:
err["query_params"] = ve.errors()
if body:
body_in_kwargs = func.__annotations__.get("body")
body_model = body_in_kwargs or body
if body_model:
body_params = request.get_json()
if request_body_many:
try:
b = validate_many_models(body, body_params)
b = validate_many_models(body_model, body_params)
except ManyModelValidationError as e:
err["body_params"] = e.errors()
else:
try:
b = body(**body_params)
b = body_model(**body_params)
except TypeError:
content_type = request.headers.get("Content-Type", "").lower()
if content_type != "application/json":
Expand All @@ -152,6 +164,11 @@ def wrapper(*args, **kwargs):
err["body_params"] = ve.errors()
request.query_params = q
request.body_params = b
if query_in_kwargs:
kwargs["query"] = q
if body_in_kwargs:
kwargs["body"] = b

if err:
status_code = current_app.config.get(
"FLASK_PYDANTIC_VALIDATION_ERROR_STATUS_CODE", 400
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Expand Up @@ -85,4 +85,14 @@ def post():
]
return response_model(results=results[: query_params.limit], count=len(results))

@app.route("/search/kwargs", methods=["POST"])
@validate()
def post_kwargs(query: query_model, body: body_model):
results = [
post_model(**p)
for p in posts
if pass_search(p, body.search_term, body.exclude, query.min_views)
]
return response_model(results=results[: query.limit], count=len(results))

return app

0 comments on commit 71df60c

Please sign in to comment.