Compare commits

...

45 Commits

Author SHA1 Message Date
Sebastián Ramírez
53da56146e 🔖 Release version 0.37.0 2019-08-30 19:10:43 -05:00
Sebastián Ramírez
3799b9027e 📝 Update release notes 2019-08-30 19:09:12 -05:00
dmontagu
c70f3f1198 Add support for custom route class (#468) 2019-08-30 19:05:59 -05:00
Sebastián Ramírez
58dddc5e4f 📝 Update release notes 2019-08-30 19:02:29 -05:00
b1-luettje
c90c4fb6c1 Allow disabling Google fonts in ReDoc (#481) 2019-08-30 19:00:55 -05:00
Sebastián Ramírez
5b3df28f0c 📝 Update release notes 2019-08-30 18:59:08 -05:00
dmontagu
6c6bdb6233 🔒 Ensure skip_defaults doesn't cause extra fields to be serialized (#485) 2019-08-30 18:56:14 -05:00
Sebastián Ramírez
f156f45193 📝 Update release notes 2019-08-30 18:37:42 -05:00
Aliaksei Urbanski
f24d744a3b Enable tests for Python 3.8-dev (#465) 2019-08-30 18:34:49 -05:00
Sebastián Ramírez
937b462cdd 📝 Update release notes 2019-08-30 18:15:08 -05:00
dconathan
3025a368c6 Add support and tests for Pydantic dataclasses in response_model (#454) 2019-08-30 18:12:15 -05:00
Sebastián Ramírez
c218e0d560 📝 Update release notes 2019-08-30 17:40:14 -05:00
Pablo Marti
1ed5aa23e6 ✏️ Fix typo in oauth2-jwt.md (#447) 2019-08-30 17:35:52 -05:00
Sebastián Ramírez
106d2171d8 📝 Update release notes 2019-08-30 17:34:45 -05:00
Zoltan Papp
c5817912d2 🐛 use media_type from Body params for OpenAPI requestBody (Fixes: #431) (#439) 2019-08-30 17:32:39 -05:00
Sebastián Ramírez
a7a92bc637 📝 Update release notes 2019-08-30 17:02:40 -05:00
naxty
68d1fea961 📝 Add article: Deploying a scikit-learn model with ONNX and FastAPI (#438) 2019-08-30 17:00:00 -05:00
Sebastián Ramírez
8c6b2d5804 📝 Update release notes 2019-08-30 16:48:53 -05:00
Zoltan Papp
19c53b21c1 Allow using custom 422 validation error and use media type from response class in schema (#437)
* media_type of additional responses from the response_class

* Use HTTPValidationError only if a custom one is not defined (Fixes: #429)
2019-08-30 16:46:05 -05:00
Sebastián Ramírez
44d63cd555 📝 Update release notes 2019-08-30 16:36:18 -05:00
Sebastián Ramírez
55c4b5fb0b 🐛 Fix "default" extra response with extra status codes (#489)
* 🐛 Fix lowercase "default" extra response

* 🐛 Fix model for responses, to allow "default" plus status codes

*  Add test for "default" extra response
2019-08-30 16:34:47 -05:00
Sebastián Ramírez
c32e800c23 📝 Update release notes 2019-08-30 11:30:52 -05:00
Zoltan Papp
73dbbeab55 Allow additional responses to use status ranges and "default" (#435) 2019-08-30 11:17:42 -05:00
Sebastián Ramírez
417a3ab140 🔖 Release 0.36.0 2019-08-26 08:28:33 -05:00
Sebastián Ramírez
a3235ed8de 📝 Update release notes 2019-08-26 08:27:31 -05:00
dmontagu
38495fffa5 🐛 Fix skip_defaults implementation when returning a Pydantic model (#422) 2019-08-26 08:24:58 -05:00
Sebastián Ramírez
b77a43bcac 📝 Update release notes 2019-08-24 22:08:10 -05:00
dmontagu
483eb73b26 🐛 Use caching logic to determine OpenAPI spec for duplicate dependencies (#417) 2019-08-24 21:55:25 -05:00
Sebastián Ramírez
51a928d3f5 📝 Update release notes 2019-08-24 20:08:04 -05:00
Sebastián Ramírez
e71636e381 🐛 Fix mypy route errors after merging #415 (#462) 2019-08-24 20:05:44 -05:00
Vitaliy Kucheryaviy
f7f17fcfd6 Allow empty routed path (issue #414) (#415) 2019-08-24 19:39:48 -05:00
Sebastián Ramírez
033bc2a6c9 🔖 Release 0.35.0 2019-08-07 14:12:15 -05:00
Sebastián Ramírez
28d3b9f783 📝 Update release notes 2019-08-07 14:09:50 -05:00
Pablo Marti
0c55553328 ✏️ Fix typo in assert statement (#419) 2019-08-07 14:03:11 -05:00
Bronsen
b66056aa34 📝 Fix plural-s without apostrophe in docs (#411) 2019-08-07 14:01:31 -05:00
Sebastián Ramírez
4f10b8b98d 📝 Update release notes 2019-08-07 13:57:41 -05:00
Koudai Aono
06eb421934 Fix request body parsing with Union (#400) 2019-08-07 13:55:33 -05:00
Sebastián Ramírez
bf229ad5d8 🔖 Release 0.34.0 upgrading Starlette 2019-08-06 07:22:06 -05:00
Sebastián Ramírez
d0319001be 📝 Update Release Notes 2019-08-06 07:13:24 -05:00
David De Sousa
c4682af13d ⬆️ Upgrade Starlette max range to 0.12.7 (#367) 2019-08-06 07:10:29 -05:00
Sebastián Ramírez
6ca3ce80e4 📝 Update release notes 2019-07-12 19:15:21 -05:00
Sebastián Ramírez
25e85c8522 Add test from @dmontagu in #333 for duplicate models (#385) 2019-07-12 19:13:28 -05:00
Sebastián Ramírez
6bf3ab3b7a 🔖 Release 0.33.0, including Pydantic 0.30.0 2019-07-12 19:01:27 -05:00
Sebastián Ramírez
f5ea5eef2a 📝 Update release notes 2019-07-12 18:58:09 -05:00
James Kaplan
46a986cacf ⬆️ Upgrade Pydantic to 0.30 (#384)
* bump pydantic to 0.30

* 📌 Pin Pydantic to 0.30 as 0.31 hasn't been released
2019-07-12 18:56:25 -05:00
31 changed files with 1242 additions and 72 deletions

View File

@@ -7,6 +7,13 @@ cache: pip
python:
- "3.6"
- "3.7"
- "3.8-dev"
- "nightly"
matrix:
allow_failures:
- python: "3.8-dev"
- python: "nightly"
install:
- pip install flit

View File

@@ -25,8 +25,8 @@ sqlalchemy = "*"
uvicorn = "*"
[packages]
starlette = "==0.12.0"
pydantic = "==0.29.0"
starlette = "==0.12.7"
pydantic = "==0.30.0"
databases = {extras = ["sqlite"],version = "*"}
hypercorn = "*"

View File

@@ -193,7 +193,7 @@ But then <a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> wa
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so, the security is actually increased, by reducing their lifespan.
The domain's are securely verified and the certificates are generated automatically. This also allows automatizing the renewal of these certificates.
The domains are securely verified and the certificates are generated automatically. This also allows automatizing the renewal of these certificates.
The idea is to automatize the acquisition and renewal of these certificates, so that you can have secure HTTPS, free, forever.

View File

@@ -25,6 +25,8 @@ Here's an incomplete list of some of them.
* <a href="https://blog.bartab.fr/fastapi-logging-on-the-fly/" target="_blank">FastAPI, a simple use case on logging</a> by <a href="https://blog.bartab.fr/" target="_blank">@euri10</a>.
* <a href="https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915" target="_blank">Deploying a scikit-learn model with ONNX and FastAPI</a> by <a href="https://www.linkedin.com/in/nico-axtmann" target="_blank">Nico Axtmann</a>.
### Japanese
* <a href="https://qiita.com/mtitg/items/47770e9a562dd150631d" target="_blank">FastAPIDB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.

View File

@@ -1,5 +1,48 @@
## Latest changes
## 0.37.0
* Add support for custom route classes for advanced use cases. PR [#468](https://github.com/tiangolo/fastapi/pull/468) by [@dmontagu](https://github.com/dmontagu).
* Allow disabling Google fonts in ReDoc. PR [#481](https://github.com/tiangolo/fastapi/pull/481) by [@b1-luettje](https://github.com/b1-luettje).
* Fix security issue: when returning a sub-class of a response model and using `skip_defaults` it could leak information. PR [#485](https://github.com/tiangolo/fastapi/pull/485) by [@dmontagu](https://github.com/dmontagu).
* Enable tests for Python 3.8-dev. PR [#465](https://github.com/tiangolo/fastapi/pull/465) by [@Jamim](https://github.com/Jamim).
* Add support and tests for Pydantic dataclasses in `response_model`. PR [#454](https://github.com/tiangolo/fastapi/pull/454) by [@dconathan](https://github.com/dconathan).
* Fix typo in OAuth2 JWT tutorial. PR [#447](https://github.com/tiangolo/fastapi/pull/447) by [@pablogamboa](https://github.com/pablogamboa).
* Use the `media_type` parameter in `Body()` params to set the media type in OpenAPI for `requestBody`. PR [#439](https://github.com/tiangolo/fastapi/pull/439) by [@divums](https://github.com/divums).
* Add article [Deploying a scikit-learn model with ONNX and FastAPI](https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915) by [https://www.linkedin.com/in/nico-axtmann](Nico Axtmann). PR [#438](https://github.com/tiangolo/fastapi/pull/438) by [@naxty](https://github.com/naxty).
* Allow setting custom `422` (validation error) response/schema in OpenAPI.
* And use media type from response class instead of fixed `application/json` (the default).
* PR [#437](https://github.com/tiangolo/fastapi/pull/437) by [@divums](https://github.com/divums).
* Fix using `"default"` extra response with status codes at the same time. PR [#489](https://github.com/tiangolo/fastapi/pull/489).
* Allow additional responses to use status code ranges (like `5XX` and `4XX`) and `"default"`. PR [#435](https://github.com/tiangolo/fastapi/pull/435) by [@divums](https://github.com/divums).
## 0.36.0
* Fix implementation for `skip_defaults` when returning a Pydantic model. PR [#422](https://github.com/tiangolo/fastapi/pull/422) by [@dmontagu](https://github.com/dmontagu).
* Fix OpenAPI generation when using the same dependency in multiple places for the same *path operation*. PR [#417](https://github.com/tiangolo/fastapi/pull/417) by [@dmontagu](https://github.com/dmontagu).
* Allow having empty paths in *path operations* used with `include_router` and a `prefix`.
* This allows having a router for `/cats` and all its *path operations*, while having one of them for `/cats`.
* Now it doesn't have to be only `/cats/` (with a trailing slash).
* To use it, declare the path in the *path operation* as the empty string (`""`).
* PR [#415](https://github.com/tiangolo/fastapi/pull/415) by [@vitalik](https://github.com/vitalik).
* Fix mypy error after merging PR #415. PR [#462](https://github.com/tiangolo/fastapi/pull/462).
## 0.35.0
* Fix typo in routing `assert`. PR [#419](https://github.com/tiangolo/fastapi/pull/419) by [@pablogamboa](https://github.com/pablogamboa).
* Fix typo in docs. PR [#411](https://github.com/tiangolo/fastapi/pull/411) by [@bronsen](https://github.com/bronsen).
* Fix parsing a body type declared with `Union`. PR [#400](https://github.com/tiangolo/fastapi/pull/400) by [@koxudaxi](https://github.com/koxudaxi).
## 0.34.0
* Upgrade Starlette supported range to include the latest `0.12.7`. The new range is `0.11.1,<=0.12.7`. PR [#367](https://github.com/tiangolo/fastapi/pull/367) by [@dedsm](https://github.com/dedsm).
* Add test for OpenAPI schema with duplicate models from PR [#333](https://github.com/tiangolo/fastapi/pull/333) by [@dmontagu](https://github.com/dmontagu). PR [#385](https://github.com/tiangolo/fastapi/pull/385).
## 0.33.0
* Upgrade Pydantic version to `0.30.0`. PR [#384](https://github.com/tiangolo/fastapi/pull/384) by [@jekirl](https://github.com/jekirl).
## 0.32.0
* Fix typo in docs for features. PR [#380](https://github.com/tiangolo/fastapi/pull/380) by [@MartinoMensio](https://github.com/MartinoMensio).

View File

@@ -156,7 +156,7 @@ Then you could add permissions about that entity, like "drive" (for the car) or
And then, you could give that JWT token to a user (or bot), and he could use it to perform those actions (drive the car, or edit the blog post) without even needing to have an account, just with the JWT token your API generated for that.
Using these ideas, JWT can be used for way more sophisticate scenarios.
Using these ideas, JWT can be used for way more sophisticated scenarios.
In those cases, several of those entities could have the same ID, let's say `foo` (a user `foo`, a car `foo`, and a blog post `foo`).

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.32.0"
__version__ = "0.37.0"
from starlette.background import BackgroundTasks

View File

@@ -108,7 +108,16 @@ def get_sub_dependant(
return sub_dependant
def get_flat_dependant(dependant: Dependant) -> Dependant:
CacheKey = Tuple[Optional[Callable], Tuple[str, ...]]
def get_flat_dependant(
dependant: Dependant, *, skip_repeats: bool = False, visited: List[CacheKey] = None
) -> Dependant:
if visited is None:
visited = []
visited.append(dependant.cache_key)
flat_dependant = Dependant(
path_params=dependant.path_params.copy(),
query_params=dependant.query_params.copy(),
@@ -120,7 +129,11 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
path=dependant.path,
)
for sub_dependant in dependant.dependencies:
flat_sub = get_flat_dependant(sub_dependant)
if skip_repeats and sub_dependant.cache_key in visited:
continue
flat_sub = get_flat_dependant(
sub_dependant, skip_repeats=skip_repeats, visited=visited
)
flat_dependant.path_params.extend(flat_sub.path_params)
flat_dependant.query_params.extend(flat_sub.query_params)
flat_dependant.header_params.extend(flat_sub.header_params)
@@ -131,12 +144,17 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
def is_scalar_field(field: Field) -> bool:
return (
if not (
field.shape == Shape.SINGLETON
and not lenient_issubclass(field.type_, BaseModel)
and not lenient_issubclass(field.type_, sequence_types + (dict,))
and not isinstance(field.schema, params.Body)
)
):
return False
if field.sub_fields:
if not all(is_scalar_field(f) for f in field.sub_fields):
return False
return True
def is_scalar_sequence_field(field: Field) -> bool:
@@ -541,6 +559,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
for f in flat_dependant.body_params:
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
required = any(True for f in flat_dependant.body_params if f.required)
BodySchema_kwargs: Dict[str, Any] = dict(default=None)
if any(isinstance(f.schema, params.File) for f in flat_dependant.body_params):
BodySchema: Type[params.Body] = params.File
elif any(isinstance(f.schema, params.Form) for f in flat_dependant.body_params):
@@ -548,6 +568,14 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
else:
BodySchema = params.Body
body_param_media_types = [
getattr(f.schema, "media_type")
for f in flat_dependant.body_params
if isinstance(f.schema, params.Body)
]
if len(set(body_param_media_types)) == 1:
BodySchema_kwargs["media_type"] = body_param_media_types[0]
field = Field(
name="body",
type_=BodyModel,
@@ -556,6 +584,6 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodySchema(None),
schema=BodySchema(**BodySchema_kwargs),
)
return field

View File

@@ -56,6 +56,7 @@ def get_redoc_html(
title: str,
redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
redoc_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
with_google_fonts: bool = True,
) -> HTMLResponse:
html = f"""
<!DOCTYPE html>
@@ -65,7 +66,12 @@ def get_redoc_html(
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
"""
if with_google_fonts:
html += """
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
"""
html += f"""
<link rel="shortcut icon" href="{redoc_favicon_url}">
<!--
ReDoc doesn't change outer page styles

View File

@@ -210,10 +210,6 @@ class Response(BaseModel):
links: Optional[Dict[str, Union[Link, Reference]]] = None
class Responses(BaseModel):
default: Response
class Operation(BaseModel):
tags: Optional[List[str]] = None
summary: Optional[str] = None
@@ -222,7 +218,7 @@ class Operation(BaseModel):
operationId: Optional[str] = None
parameters: Optional[List[Union[Parameter, Reference]]] = None
requestBody: Optional[Union[RequestBody, Reference]] = None
responses: Union[Responses, Dict[str, Response]]
responses: Dict[str, Response]
# Workaround OpenAPI recursive reference
callbacks: Optional[Dict[str, Union[Dict[str, Any], Reference]]] = None
deprecated: Optional[bool] = None

View File

@@ -43,9 +43,18 @@ validation_error_response_definition = {
},
}
status_code_ranges: Dict[str, str] = {
"1XX": "Information",
"2XX": "Success",
"3XX": "Redirection",
"4XX": "Client Error",
"5XX": "Server Error",
"DEFAULT": "Default Response",
}
def get_openapi_params(dependant: Dependant) -> List[Field]:
flat_dependant = get_flat_dependant(dependant)
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
return (
flat_dependant.path_params
+ flat_dependant.query_params
@@ -71,15 +80,11 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L
def get_openapi_operation_parameters(
all_route_params: Sequence[Field]
) -> Tuple[Dict[str, Dict], List[Dict[str, Any]]]:
definitions: Dict[str, Dict] = {}
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
schema = param.schema
schema = cast(Param, schema)
if "ValidationError" not in definitions:
definitions["ValidationError"] = validation_error_definition
definitions["HTTPValidationError"] = validation_error_response_definition
parameter = {
"name": param.alias,
"in": schema.in_.value,
@@ -91,7 +96,7 @@ def get_openapi_operation_parameters(
if schema.deprecated:
parameter["deprecated"] = schema.deprecated
parameters.append(parameter)
return definitions, parameters
return parameters
def get_openapi_operation_request_body(
@@ -100,7 +105,7 @@ def get_openapi_operation_request_body(
if not body_field:
return None
assert isinstance(body_field, Field)
body_schema, _ = field_schema(
body_schema, _, _ = field_schema(
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
body_field.schema = cast(Body, body_field.schema)
@@ -150,7 +155,7 @@ def get_openapi_path(
for method in route.methods:
operation = get_openapi_operation_metadata(route=route, method=method)
parameters: List[Dict] = []
flat_dependant = get_flat_dependant(route.dependant)
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
security_definitions, operation_security = get_openapi_security_definitions(
flat_dependant=flat_dependant
)
@@ -159,10 +164,7 @@ def get_openapi_path(
if security_definitions:
security_schemes.update(security_definitions)
all_route_params = get_openapi_params(route.dependant)
validation_definitions, operation_parameters = get_openapi_operation_parameters(
all_route_params=all_route_params
)
definitions.update(validation_definitions)
operation_parameters = get_openapi_operation_parameters(all_route_params)
parameters.extend(operation_parameters)
if parameters:
operation["parameters"] = parameters
@@ -172,11 +174,6 @@ def get_openapi_path(
)
if request_body_oai:
operation["requestBody"] = request_body_oai
if "ValidationError" not in definitions:
definitions["ValidationError"] = validation_error_definition
definitions[
"HTTPValidationError"
] = validation_error_response_definition
if route.responses:
for (additional_status_code, response) in route.responses.items():
assert isinstance(
@@ -184,24 +181,27 @@ def get_openapi_path(
), "An additional response must be a dict"
field = route.response_fields.get(additional_status_code)
if field:
response_schema, _ = field_schema(
response_schema, _, _ = field_schema(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
response.setdefault("content", {}).setdefault(
"application/json", {}
route.response_class.media_type, {}
)["schema"] = response_schema
status_text = http.client.responses.get(int(additional_status_code))
status_text: Optional[str] = status_code_ranges.get(
str(additional_status_code).upper()
) or http.client.responses.get(int(additional_status_code))
response.setdefault(
"description", status_text or "Additional Response"
)
operation.setdefault("responses", {})[
str(additional_status_code)
] = response
status_code_key = str(additional_status_code).upper()
if status_code_key == "DEFAULT":
status_code_key = "default"
operation.setdefault("responses", {})[status_code_key] = response
status_code = str(route.status_code)
response_schema = {"type": "string"}
if lenient_issubclass(route.response_class, JSONResponse):
if route.response_field:
response_schema, _ = field_schema(
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
@@ -216,8 +216,15 @@ def get_openapi_path(
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
"schema"
] = response_schema
if all_route_params or route.body_field:
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
if (all_route_params or route.body_field) and not any(
[
status in operation["responses"]
for status in [http422, "4xx", "default"]
]
):
operation["responses"][http422] = {
"description": "Validation Error",
"content": {
"application/json": {
@@ -225,6 +232,13 @@ def get_openapi_path(
}
},
}
if "ValidationError" not in definitions:
definitions.update(
{
"ValidationError": validation_error_definition,
"HTTPValidationError": validation_error_response_definition,
}
)
path[method.lower()] = operation
return path, security_schemes, definitions

View File

@@ -45,6 +45,8 @@ def serialize_response(
) -> Any:
if field:
errors = []
if skip_defaults and isinstance(response, BaseModel):
response = response.dict(skip_defaults=skip_defaults)
value, errors_ = field.validate(response, {}, loc=("response",))
if isinstance(errors_, ErrorWrapper):
errors.append(errors_)
@@ -147,7 +149,7 @@ def get_websocket_app(
if errors:
await websocket.close(code=WS_1008_POLICY_VIOLATION)
raise WebSocketRequestValidationError(errors)
assert dependant.call is not None, "dependant.call must me a function"
assert dependant.call is not None, "dependant.call must be a function"
await dependant.call(**values)
return app
@@ -201,7 +203,6 @@ class APIRoute(routing.Route):
response_class: Type[Response] = JSONResponse,
dependency_overrides_provider: Any = None,
) -> None:
assert path.startswith("/"), "Routed paths must always start with '/'"
self.path = path
self.endpoint = endpoint
self.name = get_name(endpoint) if name is None else name
@@ -316,11 +317,13 @@ class APIRouter(routing.Router):
redirect_slashes: bool = True,
default: ASGIApp = None,
dependency_overrides_provider: Any = None,
route_class: Type[APIRoute] = APIRoute,
) -> None:
super().__init__(
routes=routes, redirect_slashes=redirect_slashes, default=default
)
self.dependency_overrides_provider = dependency_overrides_provider
self.route_class = route_class
def add_api_route(
self,
@@ -346,7 +349,7 @@ class APIRouter(routing.Router):
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> None:
route = APIRoute(
route = self.route_class(
path,
endpoint=endpoint,
response_model=response_model,
@@ -448,6 +451,14 @@ class APIRouter(routing.Router):
assert not prefix.endswith(
"/"
), "A path prefix must not end with '/', as the routes will start with '/'"
else:
for r in router.routes:
path = getattr(r, "path")
name = getattr(r, "name", "unknown")
if path is not None and not path:
raise Exception(
f"Prefix and path cannot be both empty (path operation: {name})"
)
if responses is None:
responses = {}
for route in router.routes:

View File

@@ -1,4 +1,5 @@
import re
from dataclasses import is_dataclass
from typing import Any, Dict, List, Sequence, Set, Type, cast
from fastapi import routing
@@ -37,7 +38,7 @@ def get_model_definitions(
) -> Dict[str, Any]:
definitions: Dict[str, Dict] = {}
for model in flat_models:
m_schema, m_definitions = model_process_schema(
m_schema, m_definitions, m_nested_models = model_process_schema(
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
definitions.update(m_definitions)
@@ -52,6 +53,8 @@ def get_path_param_names(path: str) -> Set[str]:
def create_cloned_field(field: Field) -> Field:
original_type = field.type_
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
original_type = original_type.__pydantic_model__ # type: ignore
use_type = original_type
if lenient_issubclass(original_type, BaseModel):
original_type = cast(Type[BaseModel], original_type)

View File

@@ -19,8 +19,8 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
requires = [
"starlette >=0.11.1,<=0.12.0",
"pydantic >=0.28,<=0.29.0"
"starlette >=0.11.1,<=0.12.7",
"pydantic >=0.30,<=0.30.0"
]
description-file = "README.md"
requires-python = ">=3.6"

View File

@@ -0,0 +1,40 @@
import pytest
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/a", responses={"hello": {"description": "Not a valid additional response"}})
async def a():
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a": {
"get": {
"responses": {
# this is how one would imagine the openapi schema to be
# but since the key is not valid, openapi.utils.get_openapi will raise ValueError
"hello": {"description": "Not a valid additional response"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a_get",
}
}
},
}
client = TestClient(app)
def test_openapi_schema():
with pytest.raises(ValueError):
client.get("/openapi.json")

View File

@@ -0,0 +1,100 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.testclient import TestClient
app = FastAPI()
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class Error(BaseModel):
status: str
title: str
class JsonApiError(BaseModel):
errors: typing.List[Error]
@app.get(
"/a/{id}",
response_class=JsonApiResponse,
responses={422: {"description": "Error", "model": JsonApiError}},
)
async def a(id):
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a/{id}": {
"get": {
"responses": {
"422": {
"description": "Error",
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/vnd.api+json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a__id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Id"},
"name": "id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"Error": {
"title": "Error",
"required": ["status", "title"],
"type": "object",
"properties": {
"status": {"title": "Status", "type": "string"},
"title": {"title": "Title", "type": "string"},
},
},
"JsonApiError": {
"title": "JsonApiError",
"required": ["errors"],
"type": "object",
"properties": {
"errors": {
"title": "Errors",
"type": "array",
"items": {"$ref": "#/components/schemas/Error"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -0,0 +1,85 @@
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/a/{id}")
async def a(id):
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a/{id}": {
"get": {
"responses": {
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a__id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Id"},
"name": "id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"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"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -0,0 +1,117 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.testclient import TestClient
app = FastAPI()
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class Error(BaseModel):
status: str
title: str
class JsonApiError(BaseModel):
errors: typing.List[Error]
@app.get(
"/a",
response_class=JsonApiResponse,
responses={500: {"description": "Error", "model": JsonApiError}},
)
async def a():
pass # pragma: no cover
@app.get("/b", responses={500: {"description": "Error", "model": Error}})
async def b():
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/vnd.api+json": {"schema": {}}},
},
},
"summary": "A",
"operationId": "a_a_get",
}
},
"/b": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Error"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "B",
"operationId": "b_b_get",
}
},
},
"components": {
"schemas": {
"Error": {
"title": "Error",
"required": ["status", "title"],
"type": "object",
"properties": {
"status": {"title": "Status", "type": "string"},
"title": {"title": "Title", "type": "string"},
},
},
"JsonApiError": {
"title": "JsonApiError",
"required": ["errors"],
"type": "object",
"properties": {
"errors": {
"title": "Errors",
"type": "array",
"items": {"$ref": "#/components/schemas/Error"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -10,12 +10,25 @@ async def a():
return "a"
@router.get("/b", responses={502: {"description": "Error 2"}})
@router.get(
"/b",
responses={
502: {"description": "Error 2"},
"4XX": {"description": "Error with range, upper"},
},
)
async def b():
return "b"
@router.get("/c", responses={501: {"description": "Error 3"}})
@router.get(
"/c",
responses={
"400": {"description": "Error with str"},
"5xx": {"description": "Error with range, lower"},
"default": {"description": "A default response"},
},
)
async def c():
return "c"
@@ -43,6 +56,7 @@ openapi_schema = {
"get": {
"responses": {
"502": {"description": "Error 2"},
"4XX": {"description": "Error with range, upper"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
@@ -55,11 +69,13 @@ openapi_schema = {
"/c": {
"get": {
"responses": {
"501": {"description": "Error 3"},
"400": {"description": "Error with str"},
"5XX": {"description": "Error with range, lower"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"default": {"description": "A default response"},
},
"summary": "C",
"operationId": "c_c_get",

View File

@@ -0,0 +1,23 @@
from fastapi import FastAPI
from pydantic import BaseModel
def test_get_openapi():
app = FastAPI()
class Model(BaseModel):
pass
class Model2(BaseModel):
a: Model
class Model3(BaseModel):
c: Model
d: Model2
@app.get("/", response_model=Model3)
def f():
pass # pragma: no cover
openapi = app.openapi()
assert isinstance(openapi, dict)

View File

@@ -0,0 +1,33 @@
import pytest
from fastapi import APIRouter, FastAPI
from starlette.testclient import TestClient
app = FastAPI()
router = APIRouter()
@router.get("")
def get_empty():
return ["OK"]
app.include_router(router, prefix="/prefix")
client = TestClient(app)
def test_use_empty():
with client:
response = client.get("/prefix")
assert response.json() == ["OK"]
response = client.get("/prefix/")
assert response.status_code == 404
def test_include_empty():
# if both include and router.path are empty - it should raise exception
with pytest.raises(Exception):
app.include_router(router)

View File

@@ -54,3 +54,14 @@ def test_strings_in_custom_redoc():
body_content = html.body.decode()
assert redoc_js_url in body_content
assert redoc_favicon_url in body_content
def test_google_fonts_in_generated_redoc():
body_with_google_fonts = get_redoc_html(
openapi_url="/docs", title="title"
).body.decode()
assert "fonts.googleapis.com" in body_with_google_fonts
body_without_google_fonts = get_redoc_html(
openapi_url="/docs", title="title", with_google_fonts=False
).body.decode()
assert "fonts.googleapis.com" not in body_without_google_fonts

View File

@@ -0,0 +1,103 @@
from fastapi import Depends, FastAPI, Header
from starlette.status import HTTP_200_OK
from starlette.testclient import TestClient
app = FastAPI()
def get_header(*, someheader: str = Header(...)):
return someheader
def get_something_else(*, someheader: str = Depends(get_header)):
return f"{someheader}123"
@app.get("/")
def get_deps(dep1: str = Depends(get_header), dep2: str = Depends(get_something_else)):
return {"dep1": dep1, "dep2": dep2}
client = TestClient(app)
schema = {
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {"$ref": "#/components/schemas/ValidationError"},
"title": "Detail",
"type": "array",
}
},
"title": "HTTPValidationError",
"type": "object",
},
"ValidationError": {
"properties": {
"loc": {
"items": {"type": "string"},
"title": "Location",
"type": "array",
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error " "Type", "type": "string"},
},
"required": ["loc", "msg", "type"],
"title": "ValidationError",
"type": "object",
},
}
},
"info": {"title": "Fast API", "version": "0.1.0"},
"openapi": "3.0.2",
"paths": {
"/": {
"get": {
"operationId": "get_deps__get",
"parameters": [
{
"in": "header",
"name": "someheader",
"required": True,
"schema": {"title": "Someheader", "type": "string"},
}
],
"responses": {
"200": {
"content": {"application/json": {"schema": {}}},
"description": "Successful " "Response",
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation " "Error",
},
},
"summary": "Get Deps",
}
}
},
}
def test_schema():
response = client.get("/openapi.json")
assert response.status_code == HTTP_200_OK
actual_schema = response.json()
assert actual_schema == schema
assert (
len(actual_schema["paths"]["/"]["get"]["parameters"]) == 1
) # primary goal of this test
def test_response():
response = client.get("/", headers={"someheader": "hello"})
assert response.status_code == HTTP_200_OK
assert response.json() == {"dep1": "hello", "dep2": "hello123"}

View File

@@ -0,0 +1,67 @@
import typing
from fastapi import Body, FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
media_type = "application/vnd.api+json"
# NOTE: These are not valid JSON:API resources
# but they are fine for testing requestBody with custom media_type
class Product(BaseModel):
name: str
price: float
class Shop(BaseModel):
name: str
@app.post("/products")
async def create_product(data: Product = Body(..., media_type=media_type, embed=True)):
pass # pragma: no cover
@app.post("/shops")
async def create_shop(
data: Shop = Body(..., media_type=media_type),
included: typing.List[Product] = Body([], media_type=media_type),
):
pass # pragma: no cover
create_product_request_body = {
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/Body_create_product_products_post"}
}
},
"required": True,
}
create_shop_request_body = {
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/Body_create_shop_shops_post"}
}
},
"required": True,
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
openapi_schema = response.json()
assert (
openapi_schema["paths"]["/products"]["post"]["requestBody"]
== create_product_request_body
)
assert (
openapi_schema["paths"]["/shops"]["post"]["requestBody"]
== create_shop_request_body
)

View File

@@ -1,8 +1,7 @@
from typing import List
import pytest
from fastapi import FastAPI
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
@@ -14,38 +13,45 @@ class Item(BaseModel):
owner_ids: List[int] = None
@app.get("/items/invalid", response_model=Item)
def get_invalid():
return {"name": "invalid", "price": "foo"}
@app.get("/items/valid", response_model=Item)
def get_valid():
return {"name": "valid", "price": 1.0}
@app.get("/items/innerinvalid", response_model=Item)
def get_innerinvalid():
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
@app.get("/items/coerce", response_model=Item)
def get_coerce():
return {"name": "coerce", "price": "1.0"}
@app.get("/items/invalidlist", response_model=List[Item])
def get_invalidlist():
@app.get("/items/validlist", response_model=List[Item])
def get_validlist():
return [
{"name": "foo"},
{"name": "bar", "price": "bar"},
{"name": "baz", "price": "baz"},
{"name": "bar", "price": 1.0},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]
client = TestClient(app)
def test_invalid():
with pytest.raises(ValidationError):
client.get("/items/invalid")
def test_valid():
response = client.get("/items/valid")
response.raise_for_status()
assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None}
def test_double_invalid():
with pytest.raises(ValidationError):
client.get("/items/innerinvalid")
def test_coerce():
response = client.get("/items/coerce")
response.raise_for_status()
assert response.json() == {"name": "coerce", "price": 1.0, "owner_ids": None}
def test_invalid_list():
with pytest.raises(ValidationError):
client.get("/items/invalidlist")
def test_validlist():
response = client.get("/items/validlist")
response.raise_for_status()
assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None},
{"name": "bar", "price": 1.0, "owner_ids": None},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]

View File

@@ -0,0 +1,58 @@
from typing import List
from fastapi import FastAPI
from pydantic.dataclasses import dataclass
from starlette.testclient import TestClient
app = FastAPI()
@dataclass
class Item:
name: str
price: float = None
owner_ids: List[int] = None
@app.get("/items/valid", response_model=Item)
def get_valid():
return {"name": "valid", "price": 1.0}
@app.get("/items/coerce", response_model=Item)
def get_coerce():
return {"name": "coerce", "price": "1.0"}
@app.get("/items/validlist", response_model=List[Item])
def get_validlist():
return [
{"name": "foo"},
{"name": "bar", "price": 1.0},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]
client = TestClient(app)
def test_valid():
response = client.get("/items/valid")
response.raise_for_status()
assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None}
def test_coerce():
response = client.get("/items/coerce")
response.raise_for_status()
assert response.json() == {"name": "coerce", "price": 1.0, "owner_ids": None}
def test_validlist():
response = client.get("/items/validlist")
response.raise_for_status()
assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None},
{"name": "bar", "price": 1.0, "owner_ids": None},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
]

View File

@@ -0,0 +1,33 @@
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
class SubModel(BaseModel):
a: Optional[str] = "foo"
class Model(BaseModel):
x: Optional[int]
sub: SubModel
class ModelSubclass(Model):
y: int
@app.get("/", response_model=Model, response_model_skip_defaults=True)
def get() -> ModelSubclass:
return ModelSubclass(sub={}, y=1)
client = TestClient(app)
def test_return_defaults():
response = client.get("/")
assert response.json() == {"sub": {}}

124
tests/test_union_body.py Normal file
View File

@@ -0,0 +1,124 @@
from typing import Optional, Union
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
class Item(BaseModel):
name: Optional[str] = None
class OtherItem(BaseModel):
price: int
@app.post("/items/")
def save_union_body(item: Union[OtherItem, Item]):
return {"item": item}
client = TestClient(app)
item_openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Save Union Body",
"operationId": "save_union_body_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "Item",
"anyOf": [
{"$ref": "#/components/schemas/OtherItem"},
{"$ref": "#/components/schemas/Item"},
],
}
}
},
"required": True,
},
}
}
},
"components": {
"schemas": {
"OtherItem": {
"title": "OtherItem",
"required": ["price"],
"type": "object",
"properties": {"price": {"title": "Price", "type": "integer"}},
},
"Item": {
"title": "Item",
"type": "object",
"properties": {"name": {"title": "Name", "type": "string"}},
},
"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_item_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == item_openapi_schema
def test_post_other_item():
response = client.post("/items/", json={"price": 100})
assert response.status_code == 200
assert response.json() == {"item": {"price": 100}}
def test_post_item():
response = client.post("/items/", json={"name": "Foo"})
assert response.status_code == 200
assert response.json() == {"item": {"name": "Foo"}}

View File

@@ -0,0 +1,140 @@
import sys
from typing import Optional, Union
import pytest
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
# In Python 3.6:
# u = Union[ExtendedItem, Item] == __main__.Item
# But in Python 3.7:
# u = Union[ExtendedItem, Item] == typing.Union[__main__.ExtendedItem, __main__.Item]
skip_py36 = pytest.mark.skipif(sys.version_info < (3, 7), reason="skip python3.6")
app = FastAPI()
class Item(BaseModel):
name: Optional[str] = None
class ExtendedItem(Item):
age: int
@app.post("/items/")
def save_union_different_body(item: Union[ExtendedItem, Item]):
return {"item": item}
client = TestClient(app)
inherited_item_openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Save Union Different Body",
"operationId": "save_union_different_body_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "Item",
"anyOf": [
{"$ref": "#/components/schemas/ExtendedItem"},
{"$ref": "#/components/schemas/Item"},
],
}
}
},
"required": True,
},
}
}
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"type": "object",
"properties": {"name": {"title": "Name", "type": "string"}},
},
"ExtendedItem": {
"title": "ExtendedItem",
"required": ["age"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"age": {"title": "Age", "type": "integer"},
},
},
"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"},
}
},
},
}
},
}
@skip_py36
def test_inherited_item_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == inherited_item_openapi_schema
@skip_py36
def test_post_extended_item():
response = client.post("/items/", json={"name": "Foo", "age": 5})
assert response.status_code == 200
assert response.json() == {"item": {"name": "Foo", "age": 5}}
@skip_py36
def test_post_item():
response = client.post("/items/", json={"name": "Foo"})
assert response.status_code == 200
assert response.json() == {"item": {"name": "Foo"}}

View File

@@ -0,0 +1,51 @@
from typing import List
import pytest
from fastapi import FastAPI
from pydantic import BaseModel, ValidationError
from starlette.testclient import TestClient
app = FastAPI()
class Item(BaseModel):
name: str
price: float = None
owner_ids: List[int] = None
@app.get("/items/invalid", response_model=Item)
def get_invalid():
return {"name": "invalid", "price": "foo"}
@app.get("/items/innerinvalid", response_model=Item)
def get_innerinvalid():
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
@app.get("/items/invalidlist", response_model=List[Item])
def get_invalidlist():
return [
{"name": "foo"},
{"name": "bar", "price": "bar"},
{"name": "baz", "price": "baz"},
]
client = TestClient(app)
def test_invalid():
with pytest.raises(ValidationError):
client.get("/items/invalid")
def test_double_invalid():
with pytest.raises(ValidationError):
client.get("/items/innerinvalid")
def test_invalid_list():
with pytest.raises(ValidationError):
client.get("/items/invalidlist")

View File

@@ -0,0 +1,53 @@
from typing import List
import pytest
from fastapi import FastAPI
from pydantic import ValidationError
from pydantic.dataclasses import dataclass
from starlette.testclient import TestClient
app = FastAPI()
@dataclass
class Item:
name: str
price: float = None
owner_ids: List[int] = None
@app.get("/items/invalid", response_model=Item)
def get_invalid():
return {"name": "invalid", "price": "foo"}
@app.get("/items/innerinvalid", response_model=Item)
def get_innerinvalid():
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
@app.get("/items/invalidlist", response_model=List[Item])
def get_invalidlist():
return [
{"name": "foo"},
{"name": "bar", "price": "bar"},
{"name": "baz", "price": "baz"},
]
client = TestClient(app)
def test_invalid():
with pytest.raises(ValidationError):
client.get("/items/invalid")
def test_double_invalid():
with pytest.raises(ValidationError):
client.get("/items/innerinvalid")
def test_invalid_list():
with pytest.raises(ValidationError):
client.get("/items/invalidlist")