Compare commits

...

32 Commits

Author SHA1 Message Date
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
Sebastián Ramírez
e620aeb46d 🔖 Release 0.32.0, as PR ##347 might be a breaking change
in some specific cases
2019-07-12 18:32:30 -05:00
Sebastián Ramírez
d1e2e46b80 🔖 Release 0.31.1 2019-07-12 18:30:54 -05:00
Sebastián Ramírez
b1c4a8acd5 📝 Update release notes 2019-07-12 18:29:49 -05:00
Martino Mensio
362e2cdc79 📝 Fix small typo in docs for features (#380) 2019-07-12 18:28:07 -05:00
Sebastián Ramírez
93e6a08acd 📝 Update release notes 2019-07-12 18:25:04 -05:00
Ben Williams
3ec4342282 📝 Change limit default parameter to 10 in Query docs (#366)
Rest of docs reference 10 as the default.
2019-07-12 18:22:21 -05:00
Sebastián Ramírez
dc483478eb 📝 Update release notes 2019-07-12 18:20:02 -05:00
Chris Withers
bdd251a05b 📝 Tweak wording on OAuth2 scopes (#371) 2019-07-12 18:17:34 -05:00
Sebastián Ramírez
195559ccba 📝 Update release notes 2019-06-28 21:29:29 +02:00
Sebastián Ramírez
9a71672a95 📝 Update enum examples to use str, and improve Swagger UI in examples (#351) 2019-06-28 21:27:27 +02:00
Sebastián Ramírez
7e48be1561 📝 Update release notes 2019-06-28 20:57:14 +02:00
Sebastián Ramírez
508f9ce954 🐛 Fix regression, Swagger UI with deep linking (#350) 2019-06-28 20:56:48 +02:00
Sebastián Ramírez
afbdf2546f 📝 Update release notes 2019-06-28 20:16:53 +02:00
Sebastián Ramírez
62df417807 Add test for templates in include_router path (#349) 2019-06-28 20:15:17 +02:00
Sebastián Ramírez
09d2747a70 📝 Update release notes 2019-06-28 20:00:24 +02:00
Sebastián Ramírez
d3ea6f7514 📝 Add note to docs about including same router multiple times (#348) 2019-06-28 19:54:49 +02:00
Sebastián Ramírez
02187636ea 📝 Update release notes 2019-06-28 19:40:31 +02:00
Sebastián Ramírez
687065509b 🏗️ Fix same function names in different modules with composite bodies (#347)
* 🏗️ Implement unique IDs for dynamic models

like those used for composite bodies and responses. IDs based on path (not only on function name, as it can be duplicated in a different module).

*  Add tests for same function name and composite body

*  Update OpenAPI in tests with new dynamic model ID generation
2019-06-28 19:35:16 +02:00
43 changed files with 657 additions and 87 deletions

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

@@ -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

View File

@@ -1,5 +1,45 @@
## Latest changes
## 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`.

View File

@@ -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"

View File

@@ -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]

View File

@@ -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.

View File

@@ -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:

View File

@@ -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:

View File

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

View File

@@ -131,12 +131,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:

View File

@@ -40,7 +40,8 @@ def get_swagger_ui_html(
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout"
layout: "BaseLayout",
deepLinking: true
})
</script>
</body>

View File

@@ -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
@@ -96,7 +100,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 +117,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:
@@ -183,7 +184,7 @@ 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(
@@ -200,7 +201,7 @@ def get_openapi_path(
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,

View File

@@ -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
@@ -147,7 +147,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
@@ -205,12 +205,19 @@ class APIRoute(routing.Route):
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 +258,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 +274,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 +282,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 +291,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(

View File

@@ -37,7 +37,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)
@@ -93,3 +93,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

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,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

View File

View 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}

View 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}

View 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")

View 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

View 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"}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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"},
}

View File

@@ -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": {

View File

@@ -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"}},

View File

@@ -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": {

View File

@@ -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"},

View File

@@ -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"},
}

View File

@@ -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"},
}

View File

@@ -35,6 +35,7 @@ openapi_schema = {
"schema": {
"title": "Model_Name",
"enum": ["alexnet", "resnet", "lenet"],
"type": "string",
},
"name": "model_name",
"in": "path",

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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

View File

@@ -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
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"}}