Browse Source

Add support for tag metadata in OpenAPI (#1348)

* 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 <[email protected]>
pull/1576/head
Thomas Maschler 5 years ago
committed by GitHub
parent
commit
a071ddf3cd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. BIN
      docs/en/docs/img/tutorial/metadata/image02.png
  2. 52
      docs/en/docs/tutorial/metadata.md
  3. 28
      docs_src/metadata/tutorial004.py
  4. 3
      fastapi/applications.py
  5. 7
      fastapi/openapi/utils.py
  6. 1
      fastapi/routing.py
  7. 65
      tests/test_tutorial/test_metadata/test_tutorial004.py

BIN
docs/en/docs/img/tutorial/metadata/image02.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

52
docs/en/docs/tutorial/metadata.md

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

28
docs_src/metadata/tutorial004.py

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

3
fastapi/applications.py

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

7
fastapi/openapi/utils.py

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

1
fastapi/routing.py

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

65
tests/test_tutorial/test_metadata/test_tutorial004.py

@ -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
Loading…
Cancel
Save