Compare commits

...

14 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
12 changed files with 321 additions and 13 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

@@ -1,5 +1,21 @@
## 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).

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.32.0"
__version__ = "0.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

@@ -100,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)
@@ -184,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(
@@ -201,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

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

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)

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)

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