mirror of https://github.com/tiangolo/fastapi.git
✅ Add docs, tests and fixes for extra data types
including refactor of jsonable_encoder to allow other object and model types
This commit is contained in:
parent
75407b9295
commit
a73709507c
|
|
@ -0,0 +1,27 @@
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import Body, FastAPI
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/items/{item_id}")
|
||||||
|
async def read_items(
|
||||||
|
item_id: UUID,
|
||||||
|
start_datetime: datetime = Body(None),
|
||||||
|
end_datetime: datetime = Body(None),
|
||||||
|
repeat_at: time = Body(None),
|
||||||
|
process_after: timedelta = Body(None),
|
||||||
|
):
|
||||||
|
start_process = start_datetime + process_after
|
||||||
|
duration = end_datetime - start_process
|
||||||
|
return {
|
||||||
|
"item_id": item_id,
|
||||||
|
"start_datetime": start_datetime,
|
||||||
|
"end_datetime": end_datetime,
|
||||||
|
"repeat_at": repeat_at,
|
||||||
|
"process_after": process_after,
|
||||||
|
"start_process": start_process,
|
||||||
|
"duration": duration,
|
||||||
|
}
|
||||||
|
|
@ -116,7 +116,7 @@ Again, doing just that declaration, with **FastAPI** you get:
|
||||||
|
|
||||||
Apart from normal singular types like `str`, `int`, `float`, etc. You can use more complex singular types that inherit from `str`.
|
Apart from normal singular types like `str`, `int`, `float`, etc. You can use more complex singular types that inherit from `str`.
|
||||||
|
|
||||||
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>.
|
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>. You will see some examples in the next chapter.
|
||||||
|
|
||||||
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
|
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
Up to now, you have been using common data types, like:
|
||||||
|
|
||||||
|
* `int`
|
||||||
|
* `float`
|
||||||
|
* `str`
|
||||||
|
* `bool`
|
||||||
|
|
||||||
|
But you can also use more complex data types.
|
||||||
|
|
||||||
|
And you will still have the same features as seen up to now:
|
||||||
|
|
||||||
|
* Great editor support.
|
||||||
|
* Data conversion from incoming requests.
|
||||||
|
* Data conversion for response data.
|
||||||
|
* Data validation.
|
||||||
|
* Automatic annotation and documentation.
|
||||||
|
|
||||||
|
## Other data types
|
||||||
|
|
||||||
|
Here are some of the additional data types you can use:
|
||||||
|
|
||||||
|
* `UUID`:
|
||||||
|
* A standard "Universally Unique Identifier", common as an ID in many databases and systems.
|
||||||
|
* In requests and responses will be represented as a `str`.
|
||||||
|
* `datetime.datetime`:
|
||||||
|
* A Python `datetime.datetime`.
|
||||||
|
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `2008-09-15T15:53:00+05:00`.
|
||||||
|
* `datetime.date`:
|
||||||
|
* Python `datetime.date`.
|
||||||
|
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `2008-09-15`.
|
||||||
|
* `datetime.time`:
|
||||||
|
* A Python `datetime.time`.
|
||||||
|
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `14:23:55.003`.
|
||||||
|
* `datetime.timedelta`:
|
||||||
|
* A Python `datetime.timedelta`.
|
||||||
|
* In requests and responses will be represented as a `float` of total seconds.
|
||||||
|
* Pydantic also allows representing it as a "ISO 8601 time diff encoding", <a href="https://pydantic-docs.helpmanual.io/#json-serialisation" target="_blank">see the docs for more info</a>.
|
||||||
|
* `frozenset`:
|
||||||
|
* In requests and responses, treated the same as a `set`:
|
||||||
|
* In requests, a list will be read, eliminating duplicates and converting it to a `set`.
|
||||||
|
* In responses, the `set` will be converted to a `list`.
|
||||||
|
* The generated schema will specify that the `set` values are unique (using JSON Schema's `uniqueItems`).
|
||||||
|
* `bytes`:
|
||||||
|
* Standard Python `bytes`.
|
||||||
|
* In requests and responses will be treated as `str`.
|
||||||
|
* The generated schema will specify that it's a `str` with `binary` "format".
|
||||||
|
* `Decimal`:
|
||||||
|
* Standard Python `Decimal`.
|
||||||
|
* In requests and responses, handled the same as a `float`.
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Here's an example path operation with parameters using some of the above types.
|
||||||
|
|
||||||
|
```Python hl_lines="1 2 11 12 13 14 15"
|
||||||
|
{!./src/extra_data_types/tutorial001.py!}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like:
|
||||||
|
|
||||||
|
```Python hl_lines="17 18"
|
||||||
|
{!./src/extra_data_types/tutorial001.py!}
|
||||||
|
```
|
||||||
|
|
@ -25,7 +25,7 @@ In this case, `item_id` is declared to be an `int`.
|
||||||
!!! check
|
!!! check
|
||||||
This will give you editor support inside of your function, with error checks, completion, etc.
|
This will give you editor support inside of your function, with error checks, completion, etc.
|
||||||
|
|
||||||
## Data "parsing"
|
## Data <abbr title="also known as: serialization, parsing, marshalling">conversion</abbr>
|
||||||
|
|
||||||
If you run this example and open your browser at <a href="http://127.0.0.1:8000/items/3" target="_blank">http://127.0.0.1:8000/items/3</a>, you will see a response of:
|
If you run this example and open your browser at <a href="http://127.0.0.1:8000/items/3" target="_blank">http://127.0.0.1:8000/items/3</a>, you will see a response of:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type
|
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import params
|
from fastapi import params
|
||||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||||
|
|
@ -16,7 +19,18 @@ from pydantic.utils import lenient_issubclass
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
param_supported_types = (str, int, float, bool)
|
param_supported_types = (
|
||||||
|
str,
|
||||||
|
int,
|
||||||
|
float,
|
||||||
|
bool,
|
||||||
|
UUID,
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
time,
|
||||||
|
timedelta,
|
||||||
|
Decimal,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_sub_dependant(*, param: inspect.Parameter, path: str) -> Dependant:
|
def get_sub_dependant(*, param: inspect.Parameter, path: str) -> Dependant:
|
||||||
|
|
@ -74,7 +88,7 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||||
assert (
|
assert (
|
||||||
lenient_issubclass(param.annotation, param_supported_types)
|
lenient_issubclass(param.annotation, param_supported_types)
|
||||||
or param.annotation == param.empty
|
or param.annotation == param.empty
|
||||||
), f"Path params must be of type str, int, float or boot: {param}"
|
), f"Path params must be of one of the supported types"
|
||||||
param = signature_params[param_name]
|
param = signature_params[param_name]
|
||||||
add_param_to_fields(
|
add_param_to_fields(
|
||||||
param=param,
|
param=param,
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,64 @@ def jsonable_encoder(
|
||||||
exclude: Set[str] = set(),
|
exclude: Set[str] = set(),
|
||||||
by_alias: bool = False,
|
by_alias: bool = False,
|
||||||
include_none: bool = True,
|
include_none: bool = True,
|
||||||
|
root_encoder: bool = True,
|
||||||
|
) -> Any:
|
||||||
|
errors = []
|
||||||
|
try:
|
||||||
|
return known_data_encoder(
|
||||||
|
obj,
|
||||||
|
include=include,
|
||||||
|
exclude=exclude,
|
||||||
|
by_alias=by_alias,
|
||||||
|
include_none=include_none,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if not root_encoder:
|
||||||
|
raise e
|
||||||
|
errors.append(e)
|
||||||
|
try:
|
||||||
|
data = dict(obj)
|
||||||
|
return jsonable_encoder(
|
||||||
|
data,
|
||||||
|
include=include,
|
||||||
|
exclude=exclude,
|
||||||
|
by_alias=by_alias,
|
||||||
|
include_none=include_none,
|
||||||
|
root_encoder=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if not root_encoder:
|
||||||
|
raise e
|
||||||
|
errors.append(e)
|
||||||
|
try:
|
||||||
|
data = vars(obj)
|
||||||
|
return jsonable_encoder(
|
||||||
|
data,
|
||||||
|
include=include,
|
||||||
|
exclude=exclude,
|
||||||
|
by_alias=by_alias,
|
||||||
|
include_none=include_none,
|
||||||
|
root_encoder=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if not root_encoder:
|
||||||
|
raise e
|
||||||
|
errors.append(e)
|
||||||
|
raise ValueError(errors)
|
||||||
|
|
||||||
|
|
||||||
|
def known_data_encoder(
|
||||||
|
obj: Any,
|
||||||
|
include: Set[str] = None,
|
||||||
|
exclude: Set[str] = set(),
|
||||||
|
by_alias: bool = False,
|
||||||
|
include_none: bool = True,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if isinstance(obj, BaseModel):
|
if isinstance(obj, BaseModel):
|
||||||
return jsonable_encoder(
|
return jsonable_encoder(
|
||||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||||
include_none=include_none,
|
include_none=include_none,
|
||||||
|
root_encoder=False,
|
||||||
)
|
)
|
||||||
if isinstance(obj, Enum):
|
if isinstance(obj, Enum):
|
||||||
return obj.value
|
return obj.value
|
||||||
|
|
@ -25,8 +78,10 @@ def jsonable_encoder(
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return {
|
return {
|
||||||
jsonable_encoder(
|
jsonable_encoder(
|
||||||
key, by_alias=by_alias, include_none=include_none
|
key, by_alias=by_alias, include_none=include_none, root_encoder=False
|
||||||
): jsonable_encoder(value, by_alias=by_alias, include_none=include_none)
|
): jsonable_encoder(
|
||||||
|
value, by_alias=by_alias, include_none=include_none, root_encoder=False
|
||||||
|
)
|
||||||
for key, value in obj.items()
|
for key, value in obj.items()
|
||||||
if value is not None or include_none
|
if value is not None or include_none
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +93,7 @@ def jsonable_encoder(
|
||||||
exclude=exclude,
|
exclude=exclude,
|
||||||
by_alias=by_alias,
|
by_alias=by_alias,
|
||||||
include_none=include_none,
|
include_none=include_none,
|
||||||
|
root_encoder=False,
|
||||||
)
|
)
|
||||||
for item in obj
|
for item in obj
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||||
|
|
||||||
|
|
||||||
def serialize_response(*, field: Field = None, response: Response) -> Any:
|
def serialize_response(*, field: Field = None, response: Response) -> Any:
|
||||||
|
encoded = jsonable_encoder(response)
|
||||||
if field:
|
if field:
|
||||||
errors = []
|
errors = []
|
||||||
value, errors_ = field.validate(response, {}, loc=("response",))
|
value, errors_ = field.validate(encoded, {}, loc=("response",))
|
||||||
if isinstance(errors_, ErrorWrapper):
|
if isinstance(errors_, ErrorWrapper):
|
||||||
errors.append(errors_)
|
errors.append(errors_)
|
||||||
elif isinstance(errors_, list):
|
elif isinstance(errors_, list):
|
||||||
|
|
@ -33,7 +34,7 @@ def serialize_response(*, field: Field = None, response: Response) -> Any:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
return jsonable_encoder(value)
|
return jsonable_encoder(value)
|
||||||
else:
|
else:
|
||||||
return jsonable_encoder(response)
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
def get_app(
|
def get_app(
|
||||||
|
|
@ -86,40 +87,10 @@ def get_app(
|
||||||
raw_response = await run_in_threadpool(dependant.call, **values)
|
raw_response = await run_in_threadpool(dependant.call, **values)
|
||||||
if isinstance(raw_response, Response):
|
if isinstance(raw_response, Response):
|
||||||
return raw_response
|
return raw_response
|
||||||
if isinstance(raw_response, BaseModel):
|
response_data = serialize_response(
|
||||||
return content_type(
|
field=response_field, response=raw_response
|
||||||
content=serialize_response(
|
)
|
||||||
field=response_field, response=raw_response
|
return content_type(content=response_data, status_code=status_code)
|
||||||
),
|
|
||||||
status_code=status_code,
|
|
||||||
)
|
|
||||||
errors = []
|
|
||||||
try:
|
|
||||||
return content_type(
|
|
||||||
content=serialize_response(
|
|
||||||
field=response_field, response=raw_response
|
|
||||||
),
|
|
||||||
status_code=status_code,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(e)
|
|
||||||
try:
|
|
||||||
response = dict(raw_response)
|
|
||||||
return content_type(
|
|
||||||
content=serialize_response(field=response_field, response=response),
|
|
||||||
status_code=status_code,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(e)
|
|
||||||
try:
|
|
||||||
response = vars(raw_response)
|
|
||||||
return content_type(
|
|
||||||
content=serialize_response(field=response_field, response=response),
|
|
||||||
status_code=status_code,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(e)
|
|
||||||
raise ValueError(errors)
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ nav:
|
||||||
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
|
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
|
||||||
- Body - Schema: 'tutorial/body-schema.md'
|
- Body - Schema: 'tutorial/body-schema.md'
|
||||||
- Body - Nested Models: 'tutorial/body-nested-models.md'
|
- Body - Nested Models: 'tutorial/body-nested-models.md'
|
||||||
|
- Extra data types: 'tutorial/extra-data-types.md'
|
||||||
- Cookie Parameters: 'tutorial/cookie-params.md'
|
- Cookie Parameters: 'tutorial/cookie-params.md'
|
||||||
- Header Parameters: 'tutorial/header-params.md'
|
- Header Parameters: 'tutorial/header-params.md'
|
||||||
- Response Model: 'tutorial/response-model.md'
|
- Response Model: 'tutorial/response-model.md'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
from extra_data_types.tutorial001 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/{item_id}": {
|
||||||
|
"put": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Read Items Put",
|
||||||
|
"operationId": "read_items_items__item_id__put",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {
|
||||||
|
"title": "Item_Id",
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
},
|
||||||
|
"name": "item_id",
|
||||||
|
"in": "path",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Body_read_items"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"Body_read_items": {
|
||||||
|
"title": "Body_read_items",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"start_datetime": {
|
||||||
|
"title": "Start_Datetime",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
},
|
||||||
|
"end_datetime": {
|
||||||
|
"title": "End_Datetime",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
},
|
||||||
|
"repeat_at": {
|
||||||
|
"title": "Repeat_At",
|
||||||
|
"type": "string",
|
||||||
|
"format": "time",
|
||||||
|
},
|
||||||
|
"process_after": {
|
||||||
|
"title": "Process_After",
|
||||||
|
"type": "string",
|
||||||
|
"format": "time-delta",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"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_extra_types():
|
||||||
|
item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e"
|
||||||
|
data = {
|
||||||
|
"start_datetime": "2018-12-22T14:00:00+00:00",
|
||||||
|
"end_datetime": "2018-12-24T15:00:00+00:00",
|
||||||
|
"repeat_at": "15:30:00",
|
||||||
|
"process_after": 300,
|
||||||
|
}
|
||||||
|
expected_response = data.copy()
|
||||||
|
expected_response.update(
|
||||||
|
{
|
||||||
|
"start_process": "2018-12-22T14:05:00+00:00",
|
||||||
|
"duration": 176_100,
|
||||||
|
"item_id": item_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = client.put(f"/items/{item_id}", json=data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == expected_response
|
||||||
Loading…
Reference in New Issue