Browse Source
* ✨ Make jsonable_encoder's include and exclude receive sequences * ✨ Add include, exclude, and by_alias to app and router * ✨ Add and update tutorial code with new parameters * 📝 Update docs for new parameters and add docs for updating data * ✅ Add tests for consistency in path operation methods * ✅ Add tests for new parameters and update testspull/266/head
committed by
GitHub
17 changed files with 916 additions and 96 deletions
@ -0,0 +1,34 @@ |
|||
from typing import List |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.encoders import jsonable_encoder |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str = None |
|||
description: str = None |
|||
price: float = None |
|||
tax: float = 10.5 |
|||
tags: List[str] = [] |
|||
|
|||
|
|||
items = { |
|||
"foo": {"name": "Foo", "price": 50.2}, |
|||
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, |
|||
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, |
|||
} |
|||
|
|||
|
|||
@app.get("/items/{item_id}", response_model=Item) |
|||
async def read_item(item_id: str): |
|||
return items[item_id] |
|||
|
|||
|
|||
@app.put("/items/{item_id}", response_model=Item) |
|||
async def update_item(item_id: str, item: Item): |
|||
update_item_encoded = jsonable_encoder(item) |
|||
items[item_id] = update_item_encoded |
|||
return update_item_encoded |
@ -0,0 +1,37 @@ |
|||
from typing import List |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.encoders import jsonable_encoder |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str = None |
|||
description: str = None |
|||
price: float = None |
|||
tax: float = 10.5 |
|||
tags: List[str] = [] |
|||
|
|||
|
|||
items = { |
|||
"foo": {"name": "Foo", "price": 50.2}, |
|||
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, |
|||
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, |
|||
} |
|||
|
|||
|
|||
@app.get("/items/{item_id}", response_model=Item) |
|||
async def read_item(item_id: str): |
|||
return items[item_id] |
|||
|
|||
|
|||
@app.patch("/items/{item_id}", response_model=Item) |
|||
async def update_item(item_id: str, item: Item): |
|||
stored_item_data = items[item_id] |
|||
stored_item_model = Item(**stored_item_data) |
|||
update_data = item.dict(skip_defaults=True) |
|||
updated_item = stored_item_model.copy(update=update_data) |
|||
items[item_id] = jsonable_encoder(updated_item) |
|||
return updated_item |
@ -0,0 +1,37 @@ |
|||
from fastapi import FastAPI |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
description: str = None |
|||
price: float |
|||
tax: float = 10.5 |
|||
|
|||
|
|||
items = { |
|||
"foo": {"name": "Foo", "price": 50.2}, |
|||
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2}, |
|||
"baz": { |
|||
"name": "Baz", |
|||
"description": "There goes my baz", |
|||
"price": 50.2, |
|||
"tax": 10.5, |
|||
}, |
|||
} |
|||
|
|||
|
|||
@app.get( |
|||
"/items/{item_id}/name", |
|||
response_model=Item, |
|||
response_model_include={"name", "description"}, |
|||
) |
|||
async def read_item_name(item_id: str): |
|||
return items[item_id] |
|||
|
|||
|
|||
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"}) |
|||
async def read_item_public_data(item_id: str): |
|||
return items[item_id] |
@ -0,0 +1,37 @@ |
|||
from fastapi import FastAPI |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
description: str = None |
|||
price: float |
|||
tax: float = 10.5 |
|||
|
|||
|
|||
items = { |
|||
"foo": {"name": "Foo", "price": 50.2}, |
|||
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2}, |
|||
"baz": { |
|||
"name": "Baz", |
|||
"description": "There goes my baz", |
|||
"price": 50.2, |
|||
"tax": 10.5, |
|||
}, |
|||
} |
|||
|
|||
|
|||
@app.get( |
|||
"/items/{item_id}/name", |
|||
response_model=Item, |
|||
response_model_include=["name", "description"], |
|||
) |
|||
async def read_item_name(item_id: str): |
|||
return items[item_id] |
|||
|
|||
|
|||
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"]) |
|||
async def read_item_public_data(item_id: str): |
|||
return items[item_id] |
@ -0,0 +1,97 @@ |
|||
## Update replacing with `PUT` |
|||
|
|||
To update an item you can use the [HTTP `PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) operation. |
|||
|
|||
You can use the `jsonable_encoder` to convert the input data to data that can be stored as JSON (e.g. with a NoSQL database). For example, converting `datetime` to `str`. |
|||
|
|||
```Python hl_lines="30 31 32 33 34 35" |
|||
{!./src/body_updates/tutorial001.py!} |
|||
``` |
|||
|
|||
`PUT` is used to receive data that should replace the existing data. |
|||
|
|||
### Warning about replacing |
|||
|
|||
That means that if you want to update the item `bar` using `PUT` with a body containing: |
|||
|
|||
```Python |
|||
{ |
|||
"name": "Barz", |
|||
"price": 3, |
|||
"description": None, |
|||
} |
|||
``` |
|||
|
|||
because it doesn't include the already stored attribute `"tax": 20.2`, the input model would take the default value of `"tax": 10.5`. |
|||
|
|||
And the data would be saved with that "new" `tax` of `10.5`. |
|||
|
|||
## Partial updates with `PATCH` |
|||
|
|||
You can also use the [HTTP `PATCH`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) operation to *partially* update data. |
|||
|
|||
This means that you can send only the data that you want to update, leaving the rest intact. |
|||
|
|||
!!! Note |
|||
`PATCH` is less commonly used and known than `PUT`. |
|||
|
|||
And many teams use only `PUT`, even for partial updates. |
|||
|
|||
You are **free** to use them however you want, **FastAPI** doesn't impose any restrictions. |
|||
|
|||
But this guide shows you, more or less, how they are intended to be used. |
|||
|
|||
### Using Pydantic's `skip_defaults` parameter |
|||
|
|||
If you want to receive partial updates, it's very useful to use the parameter `skip_defaults` in Pydantic's model's `.dict()`. |
|||
|
|||
Like `item.dict(skip_defaults=True)`. |
|||
|
|||
That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. |
|||
|
|||
Then you can use this to generate a `dict` with only the data that was set, omitting default values: |
|||
|
|||
```Python hl_lines="34" |
|||
{!./src/body_updates/tutorial002.py!} |
|||
``` |
|||
|
|||
### Using Pydantic's `update` parameter |
|||
|
|||
Now, you can create a copy of the existing model using `.copy()`, and pass the `update` parameter with a `dict` containing the data to update. |
|||
|
|||
Like `stored_item_model.copy(update=update_data)`: |
|||
|
|||
```Python hl_lines="35" |
|||
{!./src/body_updates/tutorial002.py!} |
|||
``` |
|||
|
|||
### Partial updates recap |
|||
|
|||
In summary, to apply partial updates you would: |
|||
|
|||
* (Optionally) use `PATCH` instead of `PUT`. |
|||
* Retrieve the stored data. |
|||
* Put that data in a Pydantic model. |
|||
* Generate a `dict` without default values from the input model (using `skip_defaults`). |
|||
* This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model. |
|||
* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter). |
|||
* Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`). |
|||
* This is comparable to using the model's `.dict()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`. |
|||
* Save the data to your DB. |
|||
* Return the updated model. |
|||
|
|||
```Python hl_lines="30 31 32 33 34 35 36 37" |
|||
{!./src/body_updates/tutorial002.py!} |
|||
``` |
|||
|
|||
!!! tip |
|||
You can actually use this same technique with an HTTP `PUT` operation. |
|||
|
|||
But the example here uses `PATCH` because it was created for these use cases. |
|||
|
|||
!!! note |
|||
Notice that the input model is still validated. |
|||
|
|||
So, if you want to receive partial updates that can omit all the attributes, you need to have a model with all the attributes marked as optional (with default values or `None`). |
|||
|
|||
To distinguish from the models with all optional values for **updates** and models with required values for **creation**, you can use the ideas described in <a href="https://fastapi.tiangolo.com/tutorial/extra-models/" target="_blank">Extra Models</a>. |
@ -0,0 +1,22 @@ |
|||
import inspect |
|||
|
|||
from fastapi import APIRouter, FastAPI |
|||
|
|||
method_names = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] |
|||
|
|||
|
|||
def test_signatures_consistency(): |
|||
base_sig = inspect.signature(APIRouter.get) |
|||
for method_name in method_names: |
|||
router_method = getattr(APIRouter, method_name) |
|||
app_method = getattr(FastAPI, method_name) |
|||
router_sig = inspect.signature(router_method) |
|||
app_sig = inspect.signature(app_method) |
|||
param: inspect.Parameter |
|||
for key, param in base_sig.parameters.items(): |
|||
router_param: inspect.Parameter = router_sig.parameters[key] |
|||
app_param: inspect.Parameter = app_sig.parameters[key] |
|||
assert param.annotation == router_param.annotation |
|||
assert param.annotation == app_param.annotation |
|||
assert param.default == router_param.default |
|||
assert param.default == app_param.default |
@ -0,0 +1,162 @@ |
|||
from starlette.testclient import TestClient |
|||
|
|||
from body_updates.tutorial001 import app |
|||
|
|||
client = TestClient(app) |
|||
|
|||
openapi_schema = { |
|||
"openapi": "3.0.2", |
|||
"info": {"title": "Fast API", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/{item_id}": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Item", |
|||
"operationId": "read_item_items__item_id__get", |
|||
"parameters": [ |
|||
{ |
|||
"required": True, |
|||
"schema": {"title": "Item_Id", "type": "string"}, |
|||
"name": "item_id", |
|||
"in": "path", |
|||
} |
|||
], |
|||
}, |
|||
"put": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Update Item", |
|||
"operationId": "update_item_items__item_id__put", |
|||
"parameters": [ |
|||
{ |
|||
"required": True, |
|||
"schema": {"title": "Item_Id", "type": "string"}, |
|||
"name": "item_id", |
|||
"in": "path", |
|||
} |
|||
], |
|||
"requestBody": { |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"Item": { |
|||
"title": "Item", |
|||
"type": "object", |
|||
"properties": { |
|||
"name": {"title": "Name", "type": "string"}, |
|||
"description": {"title": "Description", "type": "string"}, |
|||
"price": {"title": "Price", "type": "number"}, |
|||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|||
"tags": { |
|||
"title": "Tags", |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
"default": [], |
|||
}, |
|||
}, |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
|
|||
|
|||
def test_openapi_schema(): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200 |
|||
assert response.json() == openapi_schema |
|||
|
|||
|
|||
def test_get(): |
|||
response = client.get("/items/baz") |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"name": "Baz", |
|||
"description": None, |
|||
"price": 50.2, |
|||
"tax": 10.5, |
|||
"tags": [], |
|||
} |
|||
|
|||
|
|||
def test_put(): |
|||
response = client.put( |
|||
"/items/bar", json={"name": "Barz", "price": 3, "description": None} |
|||
) |
|||
assert response.json() == { |
|||
"name": "Barz", |
|||
"description": None, |
|||
"price": 3, |
|||
"tax": 10.5, |
|||
"tags": [], |
|||
} |
@ -0,0 +1,142 @@ |
|||
from starlette.testclient import TestClient |
|||
|
|||
from response_model.tutorial005 import app |
|||
|
|||
client = TestClient(app) |
|||
|
|||
openapi_schema = { |
|||
"openapi": "3.0.2", |
|||
"info": {"title": "Fast API", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/{item_id}/name": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Item Name", |
|||
"operationId": "read_item_name_items__item_id__name_get", |
|||
"parameters": [ |
|||
{ |
|||
"required": True, |
|||
"schema": {"title": "Item_Id", "type": "string"}, |
|||
"name": "item_id", |
|||
"in": "path", |
|||
} |
|||
], |
|||
} |
|||
}, |
|||
"/items/{item_id}/public": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Item Public Data", |
|||
"operationId": "read_item_public_data_items__item_id__public_get", |
|||
"parameters": [ |
|||
{ |
|||
"required": True, |
|||
"schema": {"title": "Item_Id", "type": "string"}, |
|||
"name": "item_id", |
|||
"in": "path", |
|||
} |
|||
], |
|||
} |
|||
}, |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"Item": { |
|||
"title": "Item", |
|||
"required": ["name", "price"], |
|||
"type": "object", |
|||
"properties": { |
|||
"name": {"title": "Name", "type": "string"}, |
|||
"price": {"title": "Price", "type": "number"}, |
|||
"description": {"title": "Description", "type": "string"}, |
|||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|||
}, |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
|
|||
|
|||
def test_openapi_schema(): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200 |
|||
assert response.json() == openapi_schema |
|||
|
|||
|
|||
def test_read_item_name(): |
|||
response = client.get("/items/bar/name") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"name": "Bar", "description": "The Bar fighters"} |
|||
|
|||
|
|||
def test_read_item_public_data(): |
|||
response = client.get("/items/bar/public") |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"name": "Bar", |
|||
"description": "The Bar fighters", |
|||
"price": 62, |
|||
} |
@ -0,0 +1,142 @@ |
|||
from starlette.testclient import TestClient |
|||
|
|||
from response_model.tutorial006 import app |
|||
|
|||
client = TestClient(app) |
|||
|
|||
openapi_schema = { |
|||
"openapi": "3.0.2", |
|||
"info": {"title": "Fast API", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/{item_id}/name": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Item Name", |
|||
"operationId": "read_item_name_items__item_id__name_get", |
|||
"parameters": [ |
|||
{ |
|||
"required": True, |
|||
"schema": {"title": "Item_Id", "type": "string"}, |
|||
"name": "item_id", |
|||
"in": "path", |
|||
} |
|||
], |
|||
} |
|||
}, |
|||
"/items/{item_id}/public": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Item"} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Read Item Public Data", |
|||
"operationId": "read_item_public_data_items__item_id__public_get", |
|||
"parameters": [ |
|||
{ |
|||
"required": True, |
|||
"schema": {"title": "Item_Id", "type": "string"}, |
|||
"name": "item_id", |
|||
"in": "path", |
|||
} |
|||
], |
|||
} |
|||
}, |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"Item": { |
|||
"title": "Item", |
|||
"required": ["name", "price"], |
|||
"type": "object", |
|||
"properties": { |
|||
"name": {"title": "Name", "type": "string"}, |
|||
"price": {"title": "Price", "type": "number"}, |
|||
"description": {"title": "Description", "type": "string"}, |
|||
"tax": {"title": "Tax", "type": "number", "default": 10.5}, |
|||
}, |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
|
|||
|
|||
def test_openapi_schema(): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200 |
|||
assert response.json() == openapi_schema |
|||
|
|||
|
|||
def test_read_item_name(): |
|||
response = client.get("/items/bar/name") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"name": "Bar", "description": "The Bar fighters"} |
|||
|
|||
|
|||
def test_read_item_public_data(): |
|||
response = client.get("/items/bar/public") |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"name": "Bar", |
|||
"description": "The Bar fighters", |
|||
"price": 62, |
|||
} |
Loading…
Reference in new issue