mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 00:01:03 -05:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53da56146e | ||
|
|
3799b9027e | ||
|
|
c70f3f1198 | ||
|
|
58dddc5e4f | ||
|
|
c90c4fb6c1 | ||
|
|
5b3df28f0c | ||
|
|
6c6bdb6233 | ||
|
|
f156f45193 | ||
|
|
f24d744a3b | ||
|
|
937b462cdd | ||
|
|
3025a368c6 | ||
|
|
c218e0d560 | ||
|
|
1ed5aa23e6 | ||
|
|
106d2171d8 | ||
|
|
c5817912d2 | ||
|
|
a7a92bc637 | ||
|
|
68d1fea961 | ||
|
|
8c6b2d5804 | ||
|
|
19c53b21c1 | ||
|
|
44d63cd555 | ||
|
|
55c4b5fb0b | ||
|
|
c32e800c23 | ||
|
|
73dbbeab55 | ||
|
|
417a3ab140 | ||
|
|
a3235ed8de | ||
|
|
38495fffa5 | ||
|
|
b77a43bcac | ||
|
|
483eb73b26 | ||
|
|
51a928d3f5 | ||
|
|
e71636e381 | ||
|
|
f7f17fcfd6 | ||
|
|
033bc2a6c9 | ||
|
|
28d3b9f783 | ||
|
|
0c55553328 | ||
|
|
b66056aa34 | ||
|
|
4f10b8b98d | ||
|
|
06eb421934 | ||
|
|
bf229ad5d8 | ||
|
|
d0319001be | ||
|
|
c4682af13d | ||
|
|
6ca3ce80e4 | ||
|
|
25e85c8522 | ||
|
|
6bf3ab3b7a | ||
|
|
f5ea5eef2a | ||
|
|
46a986cacf | ||
|
|
e620aeb46d | ||
|
|
d1e2e46b80 | ||
|
|
b1c4a8acd5 | ||
|
|
362e2cdc79 | ||
|
|
93e6a08acd | ||
|
|
3ec4342282 | ||
|
|
dc483478eb | ||
|
|
bdd251a05b | ||
|
|
195559ccba | ||
|
|
9a71672a95 | ||
|
|
7e48be1561 | ||
|
|
508f9ce954 | ||
|
|
afbdf2546f | ||
|
|
62df417807 | ||
|
|
09d2747a70 | ||
|
|
d3ea6f7514 | ||
|
|
02187636ea | ||
|
|
687065509b |
@@ -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
|
||||
|
||||
4
Pipfile
4
Pipfile
@@ -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 = "*"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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">FastAPI|DB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.
|
||||
|
||||
@@ -37,7 +37,7 @@ from datetime import date
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Declare a variable as an str
|
||||
# Declare a variable as a str
|
||||
# and get editor support inside the function
|
||||
def main(user_id: str):
|
||||
return user_id
|
||||
|
||||
@@ -1,5 +1,72 @@
|
||||
## 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).
|
||||
|
||||
* Fix source code `limit` for example in [Query Parameters](https://fastapi.tiangolo.com/tutorial/query-params/). PR [#366](https://github.com/tiangolo/fastapi/pull/366) by [@Smashman](https://github.com/Smashman).
|
||||
|
||||
* Update wording in docs about [OAuth2 scopes](https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/). PR [#371](https://github.com/tiangolo/fastapi/pull/371) by [@cjw296](https://github.com/cjw296).
|
||||
|
||||
* Update docs for `Enum`s to inherit from `str` and improve Swagger UI rendering. PR [#351](https://github.com/tiangolo/fastapi/pull/351).
|
||||
|
||||
* Fix regression, add Swagger UI deep linking again. PR [#350](https://github.com/tiangolo/fastapi/pull/350).
|
||||
|
||||
* Add test for having path templates in `prefix` of `.include_router`. PR [#349](https://github.com/tiangolo/fastapi/pull/349).
|
||||
|
||||
* Add note to docs: [Include the same router multiple times with different `prefix`](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-the-same-router-multiple-times-with-different-prefix). PR [#348](https://github.com/tiangolo/fastapi/pull/348).
|
||||
|
||||
* Fix OpenAPI/JSON Schema generation for two functions with the same name (in different modules) with the same composite bodies.
|
||||
* Composite bodies' IDs are now based on path, not only on route name, as the auto-generated name uses the function names, that can be duplicated in different modules.
|
||||
* The same new ID generation applies to response models.
|
||||
* This also changes the generated title for those models.
|
||||
* Only composite bodies and response models are affected because those are generated dynamically, they don't have a module (a Python file).
|
||||
* This also adds the possibility of using `.include_router()` with the same `APIRouter` *multiple* times, with different prefixes, e.g. `/api/v2` and `/api/latest`, and it will now work correctly.
|
||||
* PR [#347](https://github.com/tiangolo/fastapi/pull/347).
|
||||
|
||||
## 0.31.0
|
||||
|
||||
* Upgrade Pydantic supported version to `0.29.0`.
|
||||
|
||||
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
class ModelName(Enum):
|
||||
class ModelName(str, Enum):
|
||||
alexnet = "alexnet"
|
||||
resnet = "resnet"
|
||||
lenet = "lenet"
|
||||
|
||||
@@ -6,5 +6,5 @@ fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_item(skip: int = 0, limit: int = 100):
|
||||
async def read_item(skip: int = 0, limit: int = 10):
|
||||
return fake_items_db[skip : skip + limit]
|
||||
|
||||
@@ -174,7 +174,6 @@ from app.routers import items, users
|
||||
|
||||
To learn more about Python Packages and Modules, read <a href="https://docs.python.org/3/tutorial/modules.html" target="_blank">the official Python documentation about Modules</a>.
|
||||
|
||||
|
||||
### Avoid name collisions
|
||||
|
||||
We are importing the submodule `items` directly, instead of importing just its variable `router`.
|
||||
@@ -216,7 +215,6 @@ It will include all the routes from that router as part of it.
|
||||
|
||||
So, behind the scenes, it will actually work as if everything was the same single app.
|
||||
|
||||
|
||||
!!! check
|
||||
You don't have to worry about performance when including routers.
|
||||
|
||||
@@ -295,7 +293,6 @@ The end result is that the item paths are now:
|
||||
|
||||
As we cannot just isolate them and "mount" them independently of the rest, the path operations are "cloned" (re-created), not included directly.
|
||||
|
||||
|
||||
## Check the automatic API docs
|
||||
|
||||
Now, run `uvicorn`, using the module `app.main` and the variable `app`:
|
||||
@@ -309,3 +306,11 @@ And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http:/
|
||||
You will see the automatic API docs, including the paths from all the submodules, using the correct paths (and prefixes) and the correct tags:
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/image01.png">
|
||||
|
||||
## Include the same router multiple times with different `prefix`
|
||||
|
||||
You can also use `.include_router()` multiple times with the *same* router using different prefixes.
|
||||
|
||||
This could be useful, for example, to expose the same API under different prefixes, e.g. `/api/v1` and `/api/latest`.
|
||||
|
||||
This is an advanced usage that you might not really need, but it's there in case you do.
|
||||
|
||||
@@ -119,7 +119,9 @@ If you have a *path operation* that receives a *path parameter*, but you want th
|
||||
|
||||
### Create an `Enum` class
|
||||
|
||||
Import `Enum` and create a sub-class that inherits from it.
|
||||
Import `Enum` and create a sub-class that inherits from `str` and from `Enum`.
|
||||
|
||||
By inheriting from `str` the API docs will be able to know that the values must be of type `string` and will be able to render correctly.
|
||||
|
||||
And create class attributes with fixed values, those fixed values will be the available valid values:
|
||||
|
||||
|
||||
@@ -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`).
|
||||
|
||||
|
||||
@@ -176,9 +176,9 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these
|
||||
|
||||
Let's review again this dependency tree and the scopes.
|
||||
|
||||
As the other dependency `get_current_active_user` has as a sub-dependency this `get_current_user`, the scope `"me"` declared at `get_current_active_user` will be included in the `security_scopes.scopes` `list` inside of `get_current_user`.
|
||||
As the `get_current_active_user` dependency has as a sub-dependency on `get_current_user`, the scope `"me"` declared at `get_current_active_user` will be included in the list of required scopes in the `security_scopes.scopes` passed to `get_current_user`.
|
||||
|
||||
And as the *path operation* itself also declares a scope `"items"`, it will also be part of this `list` `security_scopes.scopes` in `get_current_user`.
|
||||
The *path operation* itself also declares a scope, `"items"`, so this will also be in the list of `security_scopes.scopes` passed to `get_current_user`.
|
||||
|
||||
Here's how the hierarchy of dependencies and scopes looks like:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.31.0"
|
||||
__version__ = "0.37.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,7 +40,8 @@ def get_swagger_ui_html(
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout"
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
@@ -55,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>
|
||||
@@ -64,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,11 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
|
||||
from fastapi.openapi.models import OpenAPI
|
||||
from fastapi.params import Body, Param
|
||||
from fastapi.utils import get_flat_models_from_routes, get_model_definitions
|
||||
from fastapi.utils import (
|
||||
generate_operation_id_for_path,
|
||||
get_flat_models_from_routes,
|
||||
get_model_definitions,
|
||||
)
|
||||
from pydantic.fields import Field
|
||||
from pydantic.schema import field_schema, get_model_name_map
|
||||
from pydantic.utils import lenient_issubclass
|
||||
@@ -39,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
|
||||
@@ -67,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,
|
||||
@@ -87,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(
|
||||
@@ -96,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)
|
||||
@@ -113,10 +122,7 @@ def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
|
||||
if route.operation_id:
|
||||
return route.operation_id
|
||||
path: str = route.path_format
|
||||
operation_id = route.name + path
|
||||
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
return generate_operation_id_for_path(name=route.name, path=path, method=method)
|
||||
|
||||
|
||||
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
||||
@@ -149,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
|
||||
)
|
||||
@@ -158,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
|
||||
@@ -171,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(
|
||||
@@ -183,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,
|
||||
@@ -215,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": {
|
||||
@@ -224,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
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi.dependencies.utils import (
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.utils import create_cloned_field
|
||||
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import Field
|
||||
@@ -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,16 +203,22 @@ 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
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
if methods is None:
|
||||
methods = ["GET"]
|
||||
self.methods = set([method.upper() for method in methods])
|
||||
self.unique_id = generate_operation_id_for_path(
|
||||
name=self.name, path=self.path_format, method=list(methods)[0]
|
||||
)
|
||||
self.response_model = response_model
|
||||
if self.response_model:
|
||||
assert lenient_issubclass(
|
||||
response_class, JSONResponse
|
||||
), "To declare a type the response must be a JSON response"
|
||||
response_name = "Response_" + self.name
|
||||
response_name = "Response_" + self.unique_id
|
||||
self.response_field: Optional[Field] = Field(
|
||||
name=response_name,
|
||||
type_=self.response_model,
|
||||
@@ -251,7 +259,7 @@ class APIRoute(routing.Route):
|
||||
assert lenient_issubclass(
|
||||
model, BaseModel
|
||||
), "A response model must be a Pydantic model"
|
||||
response_name = f"Response_{additional_status_code}_{self.name}"
|
||||
response_name = f"Response_{additional_status_code}_{self.unique_id}"
|
||||
response_field = Field(
|
||||
name=response_name,
|
||||
type_=model,
|
||||
@@ -267,9 +275,6 @@ class APIRoute(routing.Route):
|
||||
else:
|
||||
self.response_fields = {}
|
||||
self.deprecated = deprecated
|
||||
if methods is None:
|
||||
methods = ["GET"]
|
||||
self.methods = set([method.upper() for method in methods])
|
||||
self.operation_id = operation_id
|
||||
self.response_model_include = response_model_include
|
||||
self.response_model_exclude = response_model_exclude
|
||||
@@ -278,7 +283,6 @@ class APIRoute(routing.Route):
|
||||
self.include_in_schema = include_in_schema
|
||||
self.response_class = response_class
|
||||
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
assert inspect.isfunction(endpoint) or inspect.ismethod(
|
||||
endpoint
|
||||
), f"An endpoint must be a function or method"
|
||||
@@ -288,7 +292,7 @@ class APIRoute(routing.Route):
|
||||
0,
|
||||
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.name)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.app = request_response(
|
||||
get_app(
|
||||
@@ -313,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,
|
||||
@@ -343,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,
|
||||
@@ -445,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:
|
||||
|
||||
@@ -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)
|
||||
@@ -93,3 +96,10 @@ def create_cloned_field(field: Field) -> Field:
|
||||
new_field.shape = field.shape
|
||||
new_field._populate_validators()
|
||||
return new_field
|
||||
|
||||
|
||||
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
|
||||
operation_id = name + path
|
||||
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
|
||||
@@ -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"
|
||||
|
||||
40
tests/test_additional_responses_bad.py
Normal file
40
tests/test_additional_responses_bad.py
Normal 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")
|
||||
100
tests/test_additional_responses_custom_validationerror.py
Normal file
100
tests/test_additional_responses_custom_validationerror.py
Normal 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
|
||||
85
tests/test_additional_responses_default_validationerror.py
Normal file
85
tests/test_additional_responses_default_validationerror.py
Normal 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
|
||||
117
tests/test_additional_responses_response_class.py
Normal file
117
tests/test_additional_responses_response_class.py
Normal 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
|
||||
@@ -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",
|
||||
|
||||
23
tests/test_duplicate_models_openapi.py
Normal file
23
tests/test_duplicate_models_openapi.py
Normal 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)
|
||||
33
tests/test_empty_router.py
Normal file
33
tests/test_empty_router.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
0
tests/test_modules_same_name_body/__init__.py
Normal file
0
tests/test_modules_same_name_body/__init__.py
Normal file
0
tests/test_modules_same_name_body/app/__init__.py
Normal file
0
tests/test_modules_same_name_body/app/__init__.py
Normal file
8
tests/test_modules_same_name_body/app/a.py
Normal file
8
tests/test_modules_same_name_body/app/a.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/compute")
|
||||
def compute(a: int = Body(...), b: str = Body(...)):
|
||||
return {"a": a, "b": b}
|
||||
8
tests/test_modules_same_name_body/app/b.py
Normal file
8
tests/test_modules_same_name_body/app/b.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter, Body
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/compute/")
|
||||
def compute(a: int = Body(...), b: str = Body(...)):
|
||||
return {"a": a, "b": b}
|
||||
8
tests/test_modules_same_name_body/app/main.py
Normal file
8
tests/test_modules_same_name_body/app/main.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from . import a, b
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.include_router(a.router, prefix="/a")
|
||||
app.include_router(b.router, prefix="/b")
|
||||
155
tests/test_modules_same_name_body/test_main.py
Normal file
155
tests/test_modules_same_name_body/test_main.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from .app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a/compute": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Compute",
|
||||
"operationId": "compute_a_compute_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_compute_a_compute_post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
"/b/compute/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Compute",
|
||||
"operationId": "compute_b_compute__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_compute_b_compute__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_compute_b_compute__post": {
|
||||
"title": "Body_compute_b_compute__post",
|
||||
"required": ["a", "b"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"title": "A", "type": "integer"},
|
||||
"b": {"title": "B", "type": "string"},
|
||||
},
|
||||
},
|
||||
"Body_compute_a_compute_post": {
|
||||
"title": "Body_compute_a_compute_post",
|
||||
"required": ["a", "b"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"title": "A", "type": "integer"},
|
||||
"b": {"title": "B", "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_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_a():
|
||||
data = {"a": 2, "b": "foo"}
|
||||
response = client.post("/a/compute", json=data)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
|
||||
def test_post_a_invalid():
|
||||
data = {"a": "bar", "b": "foo"}
|
||||
response = client.post("/a/compute", json=data)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_post_b():
|
||||
data = {"a": 2, "b": "foo"}
|
||||
response = client.post("/b/compute/", json=data)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
|
||||
def test_post_b_invalid():
|
||||
data = {"a": "bar", "b": "foo"}
|
||||
response = client.post("/b/compute/", json=data)
|
||||
assert response.status_code == 422
|
||||
103
tests/test_repeated_dependency_schema.py
Normal file
103
tests/test_repeated_dependency_schema.py
Normal 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"}
|
||||
67
tests/test_request_body_parameters_media_type.py
Normal file
67
tests/test_request_body_parameters_media_type.py
Normal 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
|
||||
)
|
||||
23
tests/test_router_prefix_with_template.py
Normal file
23
tests/test_router_prefix_with_template.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users/{id}")
|
||||
def read_user(segment: str, id: str):
|
||||
return {"segment": segment, "id": id}
|
||||
|
||||
|
||||
app.include_router(router, prefix="/{segment}")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/seg/users/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"segment": "seg", "id": "foo"}
|
||||
@@ -66,7 +66,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_read_current_user"
|
||||
"$ref": "#/components/schemas/Body_read_current_user_login_post"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -90,8 +90,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_current_user": {
|
||||
"title": "Body_read_current_user",
|
||||
"Body_read_current_user_login_post": {
|
||||
"title": "Body_read_current_user_login_post",
|
||||
"required": ["grant_type", "username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -73,7 +73,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_read_current_user"
|
||||
"$ref": "#/components/schemas/Body_read_current_user_login_post"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -97,8 +97,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_current_user": {
|
||||
"title": "Body_read_current_user",
|
||||
"Body_read_current_user_login_post": {
|
||||
"title": "Body_read_current_user_login_post",
|
||||
"required": ["grant_type", "username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -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]},
|
||||
]
|
||||
|
||||
58
tests/test_serialize_response_dataclass.py
Normal file
58
tests/test_serialize_response_dataclass.py
Normal 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]},
|
||||
]
|
||||
33
tests/test_skip_defaults.py
Normal file
33
tests/test_skip_defaults.py
Normal 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": {}}
|
||||
@@ -14,7 +14,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Notes",
|
||||
"title": "Response_Read_Notes_Notes__Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Note"},
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_update_item"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -70,8 +72,8 @@ openapi_schema = {
|
||||
"full_name": {"title": "Full_Name", "type": "string"},
|
||||
},
|
||||
},
|
||||
"Body_update_item": {
|
||||
"title": "Body_update_item",
|
||||
"Body_update_item_items__item_id__put": {
|
||||
"title": "Body_update_item_items__item_id__put",
|
||||
"required": ["item", "user", "importance"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -41,7 +41,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_update_item"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_update_item_items__item_id__put"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -71,8 +73,8 @@ openapi_schema = {
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
},
|
||||
},
|
||||
"Body_update_item": {
|
||||
"title": "Body_update_item",
|
||||
"Body_update_item_items__item_id__put": {
|
||||
"title": "Body_update_item_items__item_id__put",
|
||||
"required": ["item"],
|
||||
"type": "object",
|
||||
"properties": {"item": {"$ref": "#/components/schemas/Item"}},
|
||||
|
||||
@@ -44,7 +44,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_read_items"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_read_items_items__item_id__put"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -53,8 +55,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_items": {
|
||||
"title": "Body_read_items",
|
||||
"Body_read_items_items__item_id__put": {
|
||||
"title": "Body_read_items_items__item_id__put",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start_datetime": {
|
||||
|
||||
@@ -16,7 +16,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Item",
|
||||
"title": "Response_Read_Item_Items__Item_Id__Get",
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/PlaneItem"},
|
||||
{"$ref": "#/components/schemas/CarItem"},
|
||||
|
||||
@@ -16,7 +16,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Items",
|
||||
"title": "Response_Read_Items_Items__Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Keyword_Weights",
|
||||
"title": "Response_Read_Keyword_Weights_Keyword-Weights__Get",
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "number"},
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ openapi_schema = {
|
||||
"schema": {
|
||||
"title": "Model_Name",
|
||||
"enum": ["alexnet", "resnet", "lenet"],
|
||||
"type": "string",
|
||||
},
|
||||
"name": "model_name",
|
||||
"in": "path",
|
||||
|
||||
@@ -33,7 +33,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_create_file"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_file_files__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -64,7 +66,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_file"
|
||||
"$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -75,16 +77,16 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file": {
|
||||
"title": "Body_create_file",
|
||||
"Body_create_upload_file_uploadfile__post": {
|
||||
"title": "Body_create_upload_file_uploadfile__post",
|
||||
"required": ["file"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_file": {
|
||||
"title": "Body_create_upload_file",
|
||||
"Body_create_file_files__post": {
|
||||
"title": "Body_create_file_files__post",
|
||||
"required": ["file"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -33,7 +33,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_create_files"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_files_files__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -64,7 +66,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_upload_files"
|
||||
"$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -87,8 +89,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_files": {
|
||||
"title": "Body_create_files",
|
||||
"Body_create_upload_files_uploadfiles__post": {
|
||||
"title": "Body_create_upload_files_uploadfiles__post",
|
||||
"required": ["files"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -99,8 +101,8 @@ openapi_schema = {
|
||||
}
|
||||
},
|
||||
},
|
||||
"Body_create_upload_files": {
|
||||
"title": "Body_create_upload_files",
|
||||
"Body_create_files_files__post": {
|
||||
"title": "Body_create_files_files__post",
|
||||
"required": ["files"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -32,7 +32,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_login"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_login_login__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -42,8 +44,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_login": {
|
||||
"title": "Body_login",
|
||||
"Body_login_login__post": {
|
||||
"title": "Body_login_login__post",
|
||||
"required": ["username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -34,7 +34,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_create_file"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_file_files__post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -44,8 +46,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file": {
|
||||
"title": "Body_create_file",
|
||||
"Body_create_file_files__post": {
|
||||
"title": "Body_create_file_files__post",
|
||||
"required": ["file", "fileb", "token"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -31,7 +31,9 @@ openapi_schema = {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_login"}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_login_token_post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
@@ -54,8 +56,8 @@ openapi_schema = {
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_login": {
|
||||
"title": "Body_login",
|
||||
"Body_login_token_post": {
|
||||
"title": "Body_login_token_post",
|
||||
"required": ["username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -42,7 +42,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_login_for_access_token"
|
||||
"$ref": "#/components/schemas/Body_login_for_access_token_token_post"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -116,8 +116,8 @@ openapi_schema = {
|
||||
"token_type": {"title": "Token_Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"Body_login_for_access_token": {
|
||||
"title": "Body_login_for_access_token",
|
||||
"Body_login_for_access_token_token_post": {
|
||||
"title": "Body_login_for_access_token_token_post",
|
||||
"required": ["username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -177,6 +177,12 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def get_access_token(username="johndoe", password="secret", scope=None):
|
||||
data = {"username": username, "password": password}
|
||||
if scope:
|
||||
@@ -187,12 +193,6 @@ def get_access_token(username="johndoe", password="secret", scope=None):
|
||||
return access_token
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_login():
|
||||
response = client.post("/token", data={"username": "johndoe", "password": "secret"})
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -16,7 +16,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Users",
|
||||
"title": "Response_Read_Users_Users__Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/User"},
|
||||
}
|
||||
@@ -168,7 +168,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Items",
|
||||
"title": "Response_Read_Items_Items__Get",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
|
||||
124
tests/test_union_body.py
Normal file
124
tests/test_union_body.py
Normal 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"}}
|
||||
140
tests/test_union_inherited_body.py
Normal file
140
tests/test_union_inherited_body.py
Normal 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"}}
|
||||
51
tests/test_validate_response.py
Normal file
51
tests/test_validate_response.py
Normal 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")
|
||||
53
tests/test_validate_response_dataclass.py
Normal file
53
tests/test_validate_response_dataclass.py
Normal 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")
|
||||
Reference in New Issue
Block a user