From bac2f587b77340a1a5967d3aeea7ce125ed0a0ec Mon Sep 17 00:00:00 2001 From: Steven Kalt Date: Sun, 24 Nov 2019 09:00:51 -0500 Subject: [PATCH] :pencil: Document overriding operationId for all path operations using their function names (#642) --- .../tutorial002.py | 18 ++- .../tutorial003.py | 28 +---- .../tutorial004.py | 30 +++++ .../path-operation-advanced-configuration.md | 22 +++- .../test_tutorial002.py | 15 ++- .../test_tutorial003.py | 97 +-------------- .../test_tutorial004.py | 112 ++++++++++++++++++ 7 files changed, 200 insertions(+), 122 deletions(-) create mode 100644 docs/src/path_operation_advanced_configuration/tutorial004.py create mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py diff --git a/docs/src/path_operation_advanced_configuration/tutorial002.py b/docs/src/path_operation_advanced_configuration/tutorial002.py index dcc358e32..3aaae9b37 100644 --- a/docs/src/path_operation_advanced_configuration/tutorial002.py +++ b/docs/src/path_operation_advanced_configuration/tutorial002.py @@ -1,8 +1,24 @@ from fastapi import FastAPI +from fastapi.routing import APIRoute app = FastAPI() -@app.get("/items/", include_in_schema=False) +@app.get("/items/") async def read_items(): return [{"item_id": "Foo"}] + + +def use_route_names_as_operation_ids(app: FastAPI) -> None: + """ + Simplify operation IDs so that generated API clients have simpler function + names. + + Should be called only after all routes have been added. + """ + for route in app.routes: + if isinstance(route, APIRoute): + route.operation_id = route.name # in this case, 'read_items' + + +use_route_names_as_operation_ids(app) diff --git a/docs/src/path_operation_advanced_configuration/tutorial003.py b/docs/src/path_operation_advanced_configuration/tutorial003.py index 36bf02b11..dcc358e32 100644 --- a/docs/src/path_operation_advanced_configuration/tutorial003.py +++ b/docs/src/path_operation_advanced_configuration/tutorial003.py @@ -1,30 +1,8 @@ -from typing import Set - from fastapi import FastAPI -from pydantic import BaseModel app = FastAPI() -class Item(BaseModel): - name: str - description: str = None - price: float - tax: float = None - tags: Set[str] = [] - - -@app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(*, item: Item): - """ - Create an item with all the information: - - - **name**: each item must have a name - - **description**: a long description - - **price**: required - - **tax**: if the item doesn't have tax, you can omit this - - **tags**: a set of unique tag strings for this item - \f - :param item: User input. - """ - return item +@app.get("/items/", include_in_schema=False) +async def read_items(): + return [{"item_id": "Foo"}] diff --git a/docs/src/path_operation_advanced_configuration/tutorial004.py b/docs/src/path_operation_advanced_configuration/tutorial004.py new file mode 100644 index 000000000..36bf02b11 --- /dev/null +++ b/docs/src/path_operation_advanced_configuration/tutorial004.py @@ -0,0 +1,30 @@ +from typing import Set + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str = None + price: float + tax: float = None + tags: Set[str] = [] + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(*, item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + \f + :param item: User input. + """ + return item diff --git a/docs/tutorial/path-operation-advanced-configuration.md b/docs/tutorial/path-operation-advanced-configuration.md index b316159c5..7e17bcd1b 100644 --- a/docs/tutorial/path-operation-advanced-configuration.md +++ b/docs/tutorial/path-operation-advanced-configuration.md @@ -11,12 +11,30 @@ You would have to make sure that it is unique for each operation. {!./src/path_operation_advanced_configuration/tutorial001.py!} ``` +### Using the *path operation function* name as the operationId + +If you want to use your APIs' function names as `operationId`s, you can iterate over all of them and override each *path operation's* `operation_id` using their `APIRoute.name`. + +You should do it after adding all your *path operations*. + +```Python hl_lines="2 12 13 14 15 16 17 18 19 20 21 24" +{!./src/path_operation_advanced_configuration/tutorial002.py!} +``` + +!!! tip + If you manually call `app.openapi()`, you should update the `operationId`s before that. + +!!! warning + If you do this, you have to make sure each one of your *path operation functions* has a unique name. + + Even if they are in different modules (Python files). + ## 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`; ```Python hl_lines="6" -{!./src/path_operation_advanced_configuration/tutorial002.py!} +{!./src/path_operation_advanced_configuration/tutorial003.py!} ``` ## Advanced description from docstring @@ -28,5 +46,5 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest. ```Python hl_lines="19 20 21 22 23 24 25 26 27 28 29" -{!./src/path_operation_advanced_configuration/tutorial003.py!} +{!./src/path_operation_advanced_configuration/tutorial004.py!} ``` diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index 7818a0b96..0cd25c21b 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -7,7 +7,20 @@ client = TestClient(app) openapi_schema = { "openapi": "3.0.2", "info": {"title": "Fast API", "version": "0.1.0"}, - "paths": {}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items", + } + } + }, } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index 9fae3160f..9ff0adfb9 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -7,90 +7,7 @@ client = TestClient(app) 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": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item\n", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "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"}, - } - }, - }, - } - }, + "paths": {}, } @@ -100,13 +17,7 @@ def test_openapi_schema(): assert response.json() == openapi_schema -def test_query_params_str_validations(): - response = client.post("/items/", json={"name": "Foo", "price": 42}) +def test_get(): + response = client.get("/items/") assert response.status_code == 200 - assert response.json() == { - "name": "Foo", - "price": 42, - "description": None, - "tax": None, - "tags": [], - } + assert response.json() == [{"item_id": "Foo"}] diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py new file mode 100644 index 000000000..66065d802 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -0,0 +1,112 @@ +from starlette.testclient import TestClient + +from path_operation_advanced_configuration.tutorial004 import app + +client = TestClient(app) + +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": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item\n", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "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_query_params_str_validations(): + response = client.post("/items/", json={"name": "Foo", "price": 42}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 42, + "description": None, + "tax": None, + "tags": [], + }