From 836bb97a2d343937205a01a9829b975c48cdfcad Mon Sep 17 00:00:00 2001 From: Edouard Lavery-Plante Date: Thu, 29 Jul 2021 16:01:13 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20extensions=20?= =?UTF-8?q?and=20updates=20to=20the=20OpenAPI=20schema=20in=20path=20opera?= =?UTF-8?q?tions=20(#1922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../path-operation-advanced-configuration.md | 120 +++++++++++++++++- .../image01.png | Bin 0 -> 68080 bytes .../tutorial005.py | 8 ++ .../tutorial006.py | 41 ++++++ .../tutorial007.py | 34 +++++ fastapi/applications.py | 20 +++ fastapi/openapi/models.py | 3 + fastapi/openapi/utils.py | 2 + fastapi/routing.py | 23 ++++ tests/test_openapi_route_extensions.py | 45 +++++++ .../test_tutorial005.py | 36 ++++++ .../test_tutorial006.py | 59 +++++++++ .../test_tutorial007.py | 97 ++++++++++++++ 13 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 docs/en/docs/img/tutorial/path-operation-advanced-configuration/image01.png create mode 100644 docs_src/path_operation_advanced_configuration/tutorial005.py create mode 100644 docs_src/path_operation_advanced_configuration/tutorial006.py create mode 100644 docs_src/path_operation_advanced_configuration/tutorial007.py create mode 100644 tests/test_openapi_route_extensions.py create mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py create mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py create mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index 8d085d754..352fe0764 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -33,7 +33,7 @@ You should do it after adding all your *path operations*. ## Exclude from OpenAPI -To exclude a *path operation* from the generated OpenAPI schema (and thus, from the automatic documentation systems), use the parameter `include_in_schema` and set it to `False`; +To exclude a *path operation* from the generated OpenAPI schema (and thus, from the automatic documentation systems), use the parameter `include_in_schema` and set it to `False`: ```Python hl_lines="6" {!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!} @@ -50,3 +50,121 @@ It won't show up in the documentation, but other tools (such as Sphinx) will be ```Python hl_lines="19-29" {!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!} ``` + +## Additional Responses + +You probably have seen how to declare the `response_model` and `status_code` for a *path operation*. + +That defines the metadata about the main response of a *path operation*. + +You can also declare additional responses with their models, status codes, etc. + +There's a whole chapter here in the documentation about it, you can read it at [Additional Responses in OpenAPI](./additional-responses.md){.internal-link target=_blank}. + +## OpenAPI Extra + +When you declare a *path operation* in your application, **FastAPI** automatically generates the relevant metadata about that *path operation* to be included in the OpenAPI schema. + +!!! note "Technical details" + In the OpenAPI specification it is called the Operation Object. + +It has all the information about the *path operation* and is used to generate the automatic documentation. + +It includes the `tags`, `parameters`, `requestBody`, `responses`, etc. + +This *path operation*-specific OpenAPI schema is normally generated automatically by **FastAPI**, but you can also extend it. + +!!! tip + This is a low level extension point. + + If you only need to declare additonal responses, a more convenient way to do it is with [Additional Responses in OpenAPI](./additional-responses.md){.internal-link target=_blank}. + +You can extend the OpenAPI schema for a *path operation* using the parameter `openapi_extra`. + +### OpenAPI Extensions + +This `openapi_extra` can be helpful, for example, to declare [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial005.py!} +``` + +If you open the automatic API docs, your extension will show up at the bottom of the specific *path operation*. + + + +And if you see the resulting OpenAPI (at `/openapi.json` in your API), you will see your extension as part of the specific *path operation* too: + +```JSON hl_lines="22" +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### Custom OpenAPI *path operation* schema + +The dictionary in `openapi_extra` will be deeply merged with the automatically generated OpenAPI schema for the *path operation*. + +So, you could add additional data to the automatically generated schema. + +For example, you could decide to read and validate the request with your own code, without using the automatic features of FastAPI with Pydantic, but you could still want to define the request in the OpenAPI schema. + +You could do that with `openapi_extra`: + +```Python hl_lines="20-37 39-40" +{!../../../docs_src/path_operation_advanced_configuration/tutorial006.py!} +``` + +In this example, we didn't declare any Pydantic model. In fact, the request body is not even parsed as JSON, it is read directly as `bytes`, and the function `magic_data_reader()` would be in charge of parsing it in some way. + +Nevertheless, we can declare the expected schema for the request body. + +### Custom OpenAPI content type + +Using this same trick, you could use a Pydantic model to define the JSON Schema that is then included in the custom OpenAPI schema section for the *path operation*. + +And you could do this even if the data type in the request is not JSON. + +For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: + +```Python hl_lines="17-22 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. + +Then we use the request directly, and extract the body as `bytes`. This means that FastAPI won't even try to parse the request payload as JSON. + +And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: + +```Python hl_lines="26-33" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +!!! tip + Here we re-use the same Pydantic model. + + But the same way, we could have validated it in some other way. diff --git a/docs/en/docs/img/tutorial/path-operation-advanced-configuration/image01.png b/docs/en/docs/img/tutorial/path-operation-advanced-configuration/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..554e7c45664e4a8d12b2ac28712553f99f6ece49 GIT binary patch literal 68080 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 literal 0 HcmV?d00001 diff --git a/docs_src/path_operation_advanced_configuration/tutorial005.py b/docs_src/path_operation_advanced_configuration/tutorial005.py new file mode 100644 index 000000000..5837ad835 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial005.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/", openapi_extra={"x-aperture-labs-portal": "blue"}) +async def read_items(): + return [{"item_id": "portal-gun"}] diff --git a/docs_src/path_operation_advanced_configuration/tutorial006.py b/docs_src/path_operation_advanced_configuration/tutorial006.py new file mode 100644 index 000000000..403c3ee3f --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial006.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI, Request + +app = FastAPI() + + +def magic_data_reader(raw_body: bytes): + return { + "size": len(raw_body), + "content": { + "name": "Maaaagic", + "price": 42, + "description": "Just kiddin', no magic here. ✨", + }, + } + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"type": "string"}, + "price": {"type": "number"}, + "description": {"type": "string"}, + }, + } + } + }, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + data = magic_data_reader(raw_body) + return data diff --git a/docs_src/path_operation_advanced_configuration/tutorial007.py b/docs_src/path_operation_advanced_configuration/tutorial007.py new file mode 100644 index 000000000..d51752bb8 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial007.py @@ -0,0 +1,34 @@ +from typing import List + +import yaml +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, ValidationError + +app = FastAPI() + + +class Item(BaseModel): + name: str + tags: List[str] + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": Item.schema()}}, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + try: + data = yaml.safe_load(raw_body) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + try: + item = Item.parse_obj(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=e.errors()) + return item diff --git a/fastapi/applications.py b/fastapi/applications.py index b013e7b46..0c25026e2 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -236,6 +236,7 @@ class FastAPI(Starlette): JSONResponse ), name: Optional[str] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: self.router.add_api_route( path, @@ -260,6 +261,7 @@ class FastAPI(Starlette): include_in_schema=include_in_schema, response_class=response_class, name=name, + openapi_extra=openapi_extra, ) def api_route( @@ -286,6 +288,7 @@ class FastAPI(Starlette): include_in_schema: bool = True, response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.router.add_api_route( @@ -311,6 +314,7 @@ class FastAPI(Starlette): include_in_schema=include_in_schema, response_class=response_class, name=name, + openapi_extra=openapi_extra, ) return func @@ -379,6 +383,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.get( path, @@ -402,6 +407,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def put( @@ -428,6 +434,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.put( path, @@ -451,6 +458,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def post( @@ -477,6 +485,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.post( path, @@ -500,6 +509,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def delete( @@ -526,6 +536,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.delete( path, @@ -549,6 +560,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def options( @@ -575,6 +587,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.options( path, @@ -598,6 +611,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def head( @@ -624,6 +638,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.head( path, @@ -647,6 +662,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def patch( @@ -673,6 +689,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.patch( path, @@ -696,6 +713,7 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def trace( @@ -722,6 +740,7 @@ class FastAPI(Starlette): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.router.trace( path, @@ -745,4 +764,5 @@ class FastAPI(Starlette): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index efbd88ad7..4f55aa001 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -227,6 +227,9 @@ class Operation(BaseModel): security: Optional[List[Dict[str, List[str]]]] = None servers: Optional[List[Server]] = None + class Config: + extra = "allow" + class PathItem(BaseModel): ref: Optional[str] = Field(None, alias="$ref") diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 604ba5b00..0e73e21bf 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -317,6 +317,8 @@ def get_openapi_path( "HTTPValidationError": validation_error_response_definition, } ) + if route.openapi_extra: + deep_dict_update(operation, route.openapi_extra) path[method.lower()] = operation return path, security_schemes, definitions diff --git a/fastapi/routing.py b/fastapi/routing.py index f5fe0c0bd..0ad082341 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -320,6 +320,7 @@ class APIRoute(routing.Route): ), dependency_overrides_provider: Optional[Any] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: # normalise enums e.g. http.HTTPStatus if isinstance(status_code, enum.IntEnum): @@ -406,6 +407,7 @@ class APIRoute(routing.Route): self.dependency_overrides_provider = dependency_overrides_provider self.callbacks = callbacks self.app = request_response(self.get_route_handler()) + self.openapi_extra = openapi_extra def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: return get_request_handler( @@ -496,6 +498,7 @@ class APIRouter(routing.Router): name: Optional[str] = None, route_class_override: Optional[Type[APIRoute]] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: route_class = route_class_override or self.route_class responses = responses or {} @@ -537,6 +540,7 @@ class APIRouter(routing.Router): name=name, dependency_overrides_provider=self.dependency_overrides_provider, callbacks=current_callbacks, + openapi_extra=openapi_extra, ) self.routes.append(route) @@ -565,6 +569,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_api_route( @@ -591,6 +596,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) return func @@ -695,6 +701,7 @@ class APIRouter(routing.Router): name=route.name, route_class_override=type(route), callbacks=current_callbacks, + openapi_extra=route.openapi_extra, ) elif isinstance(route, routing.Route): methods = list(route.methods or []) # type: ignore # in Starlette @@ -742,6 +749,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -766,6 +774,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def put( @@ -792,6 +801,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -816,6 +826,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def post( @@ -842,6 +853,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -866,6 +878,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def delete( @@ -892,6 +905,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -916,6 +930,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def options( @@ -942,6 +957,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -966,6 +982,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def head( @@ -992,6 +1009,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -1016,6 +1034,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def patch( @@ -1042,6 +1061,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( path=path, @@ -1066,6 +1086,7 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) def trace( @@ -1092,6 +1113,7 @@ class APIRouter(routing.Router): response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, callbacks: Optional[List[BaseRoute]] = None, + openapi_extra: Optional[Dict[str, Any]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: return self.api_route( @@ -1117,4 +1139,5 @@ class APIRouter(routing.Router): response_class=response_class, name=name, callbacks=callbacks, + openapi_extra=openapi_extra, ) diff --git a/tests/test_openapi_route_extensions.py b/tests/test_openapi_route_extensions.py new file mode 100644 index 000000000..8a1080d69 --- /dev/null +++ b/tests/test_openapi_route_extensions.py @@ -0,0 +1,45 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/", openapi_extra={"x-custom-extension": "value"}) +def route_with_extras(): + return {} + + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "Route With Extras", + "operationId": "route_with_extras__get", + "x-custom-extension": "value", + } + }, + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {} diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py new file mode 100644 index 000000000..5042d1837 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from docs_src.path_operation_advanced_configuration.tutorial005 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "x-aperture-labs-portal": "blue", + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get(): + response = client.get("/items/") + assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py new file mode 100644 index 000000000..5533b2957 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -0,0 +1,59 @@ +from fastapi.testclient import TestClient + +from docs_src.path_operation_advanced_configuration.tutorial006 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"type": "string"}, + "price": {"type": "number"}, + "description": {"type": "string"}, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post(): + response = client.post("/items/", data=b"this is actually not validated") + assert response.status_code == 200, response.text + assert response.json() == { + "size": 30, + "content": { + "name": "Maaaagic", + "price": 42, + "description": "Just kiddin', no magic here. ✨", + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py new file mode 100644 index 000000000..cb5dbc8eb --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -0,0 +1,97 @@ +from fastapi.testclient import TestClient + +from docs_src.path_operation_advanced_configuration.tutorial007 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/x-yaml": { + "schema": { + "title": "Item", + "required": ["name", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post(): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + """ + response = client.post("/items/", data=yaml_data) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Deadpoolio", + "tags": ["x-force", "x-men", "x-avengers"], + } + + +def test_post_broken_yaml(): + yaml_data = """ + name: Deadpoolio + tags: + x - x-force + x - x-men + x - x-avengers + """ + response = client.post("/items/", data=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == {"detail": "Invalid YAML"} + + +def test_post_invalid(): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + - sneaky: object + """ + response = client.post("/items/", data=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + ] + }