]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Add support for tag metadata in OpenAPI (#1348)
authorThomas Maschler <tmaschler@wri.org>
Sat, 13 Jun 2020 11:58:06 +0000 (07:58 -0400)
committerGitHub <noreply@github.com>
Sat, 13 Jun 2020 11:58:06 +0000 (13:58 +0200)
* Allow to add OpenAPI tag descriptions

* fix type hint

* fix type hint 2

* refactor test to assure 100% coverage

* 📝 Update tags metadata example

* 📝 Update docs for tags metadata

* ✅ Move tags metadata test to tutorial subdir

* 🎨 Update format in applications

* 🍱 Update docs UI image based on new example

* 🎨 Apply formatting after solving conflicts

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
docs/en/docs/img/tutorial/metadata/image02.png [new file with mode: 0644]
docs/en/docs/tutorial/metadata.md
docs_src/metadata/tutorial004.py [new file with mode: 0644]
fastapi/applications.py
fastapi/openapi/utils.py
fastapi/routing.py
tests/test_tutorial/test_metadata/test_tutorial004.py [new file with mode: 0644]

diff --git a/docs/en/docs/img/tutorial/metadata/image02.png b/docs/en/docs/img/tutorial/metadata/image02.png
new file mode 100644 (file)
index 0000000..7f3ab0a
Binary files /dev/null and b/docs/en/docs/img/tutorial/metadata/image02.png differ
index 666fa7648b1c36b335d081409940737be65faa12..b9120c82e4adc510cc80e8000a4a0fe2fc79f5a6 100644 (file)
@@ -21,6 +21,58 @@ With this configuration, the automatic API docs would look like:
 
 <img src="/img/tutorial/metadata/image01.png">
 
+## Tag descriptions
+
+You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`.
+
+It takes a list containing one dictionary for each tag.
+
+Each dictionary can contain:
+
+* `name` (**required**): a `str` with the same tag name you use in the `tags` parameter in your *path operations* and `APIRouter`s.
+* `description`: a `str` with a short description for the tag. It can have Markdown and will be shown in the docs UI.
+* `externalDocs`: a `dict` describing external documentation with:
+    * `description`: a `str` with a short description for the external docs.
+    * `url` (**required**): a `str` with the URL for the external documentation.
+
+### Create metadata for tags
+
+Let's try that in an example with tags for `users` and `items`.
+
+Create metadata for your tags and pass it to the `openapi_tags` parameter:
+
+```Python hl_lines="3 4 5 6 7 8 9 10 11 12 13 14 15 16  18"
+{!../../../docs_src/metadata/tutorial004.py!}
+```
+
+Notice that you can use Markdown inside of the descriptions, for example "login" will be shown in bold (**login**) and "fancy" will be shown in italics (_fancy_).
+
+!!! tip
+    You don't have to add metadata for all the tags that you use.
+
+### Use your tags
+
+Use the `tags` parameter with your *path operations* (and `APIRouter`s) to assign them to different tags:
+
+```Python hl_lines="21  26"
+{!../../../docs_src/metadata/tutorial004.py!}
+```
+
+!!! info
+    Read more about tags in [Path Operation Configuration](../path-operation-configuration/#tags){.internal-link target=_blank}.
+
+### Check the docs
+
+Now, if you check the docs, they will show all the additional metadata:
+
+<img src="/img/tutorial/metadata/image02.png">
+
+### Order of tags
+
+The order of each tag metadata dictionary also defines the order shown in the docs UI.
+
+For example, even though `users` would go after `items` in alphabetical order, it is shown before them, because we added their metadata as the first dictionary in the list.
+
 ## OpenAPI URL
 
 By default, the OpenAPI schema is served at `/openapi.json`.
diff --git a/docs_src/metadata/tutorial004.py b/docs_src/metadata/tutorial004.py
new file mode 100644 (file)
index 0000000..465bd65
--- /dev/null
@@ -0,0 +1,28 @@
+from fastapi import FastAPI
+
+tags_metadata = [
+    {
+        "name": "users",
+        "description": "Operations with users. The **login** logic is also here.",
+    },
+    {
+        "name": "items",
+        "description": "Manage items. So _fancy_ they have their own docs.",
+        "externalDocs": {
+            "description": "Items external docs",
+            "url": "https://fastapi.tiangolo.com/",
+        },
+    },
+]
+
+app = FastAPI(openapi_tags=tags_metadata)
+
+
+@app.get("/users/", tags=["users"])
+async def get_users():
+    return [{"name": "Harry"}, {"name": "Ron"}]
+
+
+@app.get("/items/", tags=["items"])
+async def get_items():
+    return [{"name": "wand"}, {"name": "flying broom"}]
index 39e694fae971f78e817f80dc8b3d1425614a994d..3306aab3d95eb5de68b8a3733563b6289781947e 100644 (file)
@@ -37,6 +37,7 @@ class FastAPI(Starlette):
         description: str = "",
         version: str = "0.1.0",
         openapi_url: Optional[str] = "/openapi.json",
+        openapi_tags: Optional[List[Dict[str, Any]]] = None,
         default_response_class: Type[Response] = JSONResponse,
         docs_url: Optional[str] = "/docs",
         redoc_url: Optional[str] = "/redoc",
@@ -70,6 +71,7 @@ class FastAPI(Starlette):
         self.description = description
         self.version = version
         self.openapi_url = openapi_url
+        self.openapi_tags = openapi_tags
         # TODO: remove when discarding the openapi_prefix parameter
         if openapi_prefix:
             logger.warning(
@@ -103,6 +105,7 @@ class FastAPI(Starlette):
                 description=self.description,
                 routes=self.routes,
                 openapi_prefix=openapi_prefix,
+                tags=self.openapi_tags,
             )
         return self.openapi_schema
 
index bb2e7dff74475e9180fab4914b2562f3ec19ee3c..b6221ca202826643a500be1b88581e3ffe3e5943 100644 (file)
@@ -317,12 +317,13 @@ def get_openapi(
     openapi_version: str = "3.0.2",
     description: str = None,
     routes: Sequence[BaseRoute],
-    openapi_prefix: str = ""
+    openapi_prefix: str = "",
+    tags: Optional[List[Dict[str, Any]]] = None
 ) -> Dict:
     info = {"title": title, "version": version}
     if description:
         info["description"] = description
-    output = {"openapi": openapi_version, "info": info}
+    output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
     components: Dict[str, Dict] = {}
     paths: Dict[str, Dict] = {}
     flat_models = get_flat_models_from_routes(routes)
@@ -352,4 +353,6 @@ def get_openapi(
     if components:
         output["components"] = components
     output["paths"] = paths
+    if tags:
+        output["tags"] = tags
     return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True)
index 71a2b4d04e52b067b3f642ce8a59e506460d8842..b4560a8a4c75a52fe05b8ec7d8551631d427c02d 100644 (file)
@@ -12,7 +12,6 @@ from fastapi.dependencies.utils import (
 )
 from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
 from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
-from fastapi.logger import logger
 from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
 from fastapi.utils import (
     PYDANTIC_1,
diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py
new file mode 100644 (file)
index 0000000..1ec59d3
--- /dev/null
@@ -0,0 +1,65 @@
+from fastapi.testclient import TestClient
+
+from metadata.tutorial004 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "FastAPI", "version": "0.1.0"},
+    "paths": {
+        "/users/": {
+            "get": {
+                "tags": ["users"],
+                "summary": "Get Users",
+                "operationId": "get_users_users__get",
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    }
+                },
+            }
+        },
+        "/items/": {
+            "get": {
+                "tags": ["items"],
+                "summary": "Get Items",
+                "operationId": "get_items_items__get",
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    }
+                },
+            }
+        },
+    },
+    "tags": [
+        {
+            "name": "users",
+            "description": "Operations with users. The **login** logic is also here.",
+        },
+        {
+            "name": "items",
+            "description": "Manage items. So _fancy_ they have their own docs.",
+            "externalDocs": {
+                "description": "Items external docs",
+                "url": "https://fastapi.tiangolo.com/",
+            },
+        },
+    ],
+}
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == openapi_schema
+
+
+def test_path_operations():
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    response = client.get("/users/")
+    assert response.status_code == 200, response.text