Add swagger-ui docs and clean up swagger-ui options (#1739)

Contributes to #1531
This commit is contained in:
Robbe Sneyders
2023-10-13 20:33:22 +02:00
committed by GitHub
parent 8459c614fd
commit 415d383740
13 changed files with 189 additions and 149 deletions

View File

@@ -12,6 +12,7 @@ from starlette.types import ASGIApp, Receive, Scope, Send
from connexion.jsonifier import Jsonifier
from connexion.middleware import ConnexionMiddleware, MiddlewarePosition, SpecMiddleware
from connexion.middleware.lifespan import Lifespan
from connexion.options import SwaggerUIOptions
from connexion.resolver import Resolver
from connexion.uri_parsing import AbstractURIParser
@@ -43,7 +44,7 @@ class AbstractApp:
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
strict_validation: t.Optional[bool] = None,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
uri_parser_class: t.Optional[AbstractURIParser] = None,
validate_responses: t.Optional[bool] = None,
validator_map: t.Optional[dict] = None,
@@ -72,8 +73,8 @@ class AbstractApp:
start.
:param strict_validation: When True, extra form or query parameters not defined in the
specification result in a validation error. Defaults to False.
:param swagger_ui_options: A dict with configuration options for the swagger ui. See
:class:`options.ConnexionOptions`.
:param swagger_ui_options: Instance of :class:`options.ConnexionOptions` with
configuration options for the swagger ui.
:param uri_parser_class: Class to use for uri parsing. See :mod:`uri_parsing`.
:param validate_responses: Whether to validate responses against the specification. This has
an impact on performance. Defaults to False.
@@ -128,7 +129,7 @@ class AbstractApp:
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
strict_validation: t.Optional[bool] = None,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
uri_parser_class: t.Optional[AbstractURIParser] = None,
validate_responses: t.Optional[bool] = None,
validator_map: t.Optional[dict] = None,

View File

@@ -17,6 +17,7 @@ from connexion.jsonifier import Jsonifier
from connexion.middleware.abstract import RoutedAPI, RoutedMiddleware
from connexion.middleware.lifespan import Lifespan
from connexion.operations import AbstractOperation
from connexion.options import SwaggerUIOptions
from connexion.resolver import Resolver
from connexion.uri_parsing import AbstractURIParser
@@ -131,7 +132,7 @@ class AsyncApp(AbstractApp):
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
strict_validation: t.Optional[bool] = None,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
uri_parser_class: t.Optional[AbstractURIParser] = None,
validate_responses: t.Optional[bool] = None,
validator_map: t.Optional[dict] = None,
@@ -161,8 +162,8 @@ class AsyncApp(AbstractApp):
start.
:param strict_validation: When True, extra form or query parameters not defined in the
specification result in a validation error. Defaults to False.
:param swagger_ui_options: A dict with configuration options for the swagger ui. See
:class:`options.ConnexionOptions`.
:param swagger_ui_options: Instance of :class:`options.ConnexionOptions` with
configuration options for the swagger ui.
:param uri_parser_class: Class to use for uri parsing. See :mod:`uri_parsing`.
:param validate_responses: Whether to validate responses against the specification. This has
an impact on performance. Defaults to False.

View File

@@ -20,6 +20,7 @@ from connexion.jsonifier import Jsonifier
from connexion.middleware.abstract import AbstractRoutingAPI, SpecMiddleware
from connexion.middleware.lifespan import Lifespan
from connexion.operations import AbstractOperation
from connexion.options import SwaggerUIOptions
from connexion.problem import problem
from connexion.resolver import Resolver
from connexion.uri_parsing import AbstractURIParser
@@ -188,7 +189,7 @@ class FlaskApp(AbstractApp):
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
strict_validation: t.Optional[bool] = None,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
uri_parser_class: t.Optional[AbstractURIParser] = None,
validate_responses: t.Optional[bool] = None,
validator_map: t.Optional[dict] = None,
@@ -221,8 +222,8 @@ class FlaskApp(AbstractApp):
start.
:param strict_validation: When True, extra form or query parameters not defined in the
specification result in a validation error. Defaults to False.
:param swagger_ui_options: A dict with configuration options for the swagger ui. See
:class:`options.ConnexionOptions`.
:param swagger_ui_options: Instance of :class:`options.ConnexionOptions` with
configuration options for the swagger ui.
:param uri_parser_class: Class to use for uri parsing. See :mod:`uri_parsing`.
:param validate_responses: Whether to validate responses against the specification. This has
an impact on performance. Defaults to False.

View File

@@ -8,12 +8,16 @@ import sys
from os import path
import click
import importlib_metadata
from clickclick import AliasedGroup
import connexion
from connexion.mock import MockResolver
try:
import importlib_metadata
except ImportError:
import importlib.metadata as importlib_metadata # type: ignore
logger = logging.getLogger("connexion.cli")
FLASK_APP = "flask"

View File

@@ -21,6 +21,7 @@ from connexion.middleware.response_validation import ResponseValidationMiddlewar
from connexion.middleware.routing import RoutingMiddleware
from connexion.middleware.security import SecurityMiddleware
from connexion.middleware.swagger_ui import SwaggerUIMiddleware
from connexion.options import SwaggerUIOptions
from connexion.resolver import Resolver
from connexion.uri_parsing import AbstractURIParser
from connexion.utils import inspect_function_arguments
@@ -51,7 +52,7 @@ class _Options:
resolver_error: t.Optional[int] = None
resolver_error_handler: t.Optional[t.Callable] = field(init=False)
strict_validation: t.Optional[bool] = False
swagger_ui_options: t.Optional[dict] = None
swagger_ui_options: t.Optional[SwaggerUIOptions] = None
uri_parser_class: t.Optional[AbstractURIParser] = None
validate_responses: t.Optional[bool] = False
validator_map: t.Optional[dict] = None
@@ -186,7 +187,7 @@ class ConnexionMiddleware:
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
strict_validation: t.Optional[bool] = None,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
uri_parser_class: t.Optional[AbstractURIParser] = None,
validate_responses: t.Optional[bool] = None,
validator_map: t.Optional[dict] = None,
@@ -214,8 +215,8 @@ class ConnexionMiddleware:
start.
:param strict_validation: When True, extra form or query parameters not defined in the
specification result in a validation error. Defaults to False.
:param swagger_ui_options: A dict with configuration options for the swagger ui. See
:class:`options.ConnexionOptions`.
:param swagger_ui_options: Instance of :class:`options.ConnexionOptions` with
configuration options for the swagger ui.
:param uri_parser_class: Class to use for uri parsing. See :mod:`uri_parsing`.
:param validate_responses: Whether to validate responses against the specification. This has
an impact on performance. Defaults to False.
@@ -338,7 +339,7 @@ class ConnexionMiddleware:
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
strict_validation: t.Optional[bool] = None,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
uri_parser_class: t.Optional[AbstractURIParser] = None,
validate_responses: t.Optional[bool] = None,
validator_map: t.Optional[dict] = None,

View File

@@ -16,7 +16,7 @@ from starlette.types import ASGIApp, Receive, Scope, Send
from connexion.jsonifier import Jsonifier
from connexion.middleware import SpecMiddleware
from connexion.middleware.abstract import AbstractSpecAPI
from connexion.options import SwaggerUIOptions
from connexion.options import SwaggerUIConfig, SwaggerUIOptions
from connexion.utils import yamldumper
logger = logging.getLogger("connexion.middleware.swagger_ui")
@@ -30,13 +30,13 @@ class SwaggerUIAPI(AbstractSpecAPI):
self,
*args,
default: ASGIApp,
swagger_ui_options: t.Optional[dict] = None,
swagger_ui_options: t.Optional[SwaggerUIOptions] = None,
**kwargs
):
super().__init__(*args, **kwargs)
self.router = Router(default=default)
self.options = SwaggerUIOptions(
self.options = SwaggerUIConfig(
swagger_ui_options, oas_version=self.specification.version
)
@@ -44,11 +44,11 @@ class SwaggerUIAPI(AbstractSpecAPI):
self.add_openapi_json()
self.add_openapi_yaml()
if self.options.openapi_console_ui_available:
if self.options.swagger_ui_available:
self.add_swagger_ui()
self._templates = Jinja2Templates(
directory=str(self.options.openapi_console_ui_from_dir)
directory=str(self.options.swagger_ui_template_dir)
)
@staticmethod
@@ -121,7 +121,7 @@ class SwaggerUIAPI(AbstractSpecAPI):
"""
Adds swagger ui to {base_path}/ui/
"""
console_ui_path = self.options.openapi_console_ui_path.strip().rstrip("/")
console_ui_path = self.options.swagger_ui_path.strip().rstrip("/")
logger.debug("Adding swagger-ui: %s%s/", self.base_path, console_ui_path)
for path in (
@@ -132,7 +132,7 @@ class SwaggerUIAPI(AbstractSpecAPI):
methods=["GET"], path=path, endpoint=self._get_swagger_ui_home
)
if self.options.openapi_console_ui_config is not None:
if self.options.swagger_ui_config:
self.router.add_route(
methods=["GET"],
path=console_ui_path + "/swagger-ui-config.json",
@@ -155,7 +155,7 @@ class SwaggerUIAPI(AbstractSpecAPI):
# serve index.html, so we add the redirect above.
self.router.mount(
path=console_ui_path,
app=StaticFiles(directory=str(self.options.openapi_console_ui_from_dir)),
app=StaticFiles(directory=str(self.options.swagger_ui_template_dir)),
name="swagger_ui_static",
)
@@ -164,9 +164,9 @@ class SwaggerUIAPI(AbstractSpecAPI):
template_variables = {
"request": req,
"openapi_spec_url": (base_path + self.options.openapi_spec_path),
**self.options.openapi_console_ui_index_template_variables,
**self.options.swagger_ui_template_arguments,
}
if self.options.openapi_console_ui_config is not None:
if self.options.swagger_ui_config:
template_variables["configUrl"] = "swagger-ui-config.json"
return self._templates.TemplateResponse("index.j2", template_variables)
@@ -175,7 +175,7 @@ class SwaggerUIAPI(AbstractSpecAPI):
return StarletteResponse(
status_code=200,
media_type="application/json",
content=json.dumps(self.options.openapi_console_ui_config),
content=json.dumps(self.options.swagger_ui_config),
)

View File

@@ -1,14 +1,14 @@
"""
This module defines a Connexion specific options class to pass to the Connexion App or API.
"""
import dataclasses
import logging
from typing import Optional # NOQA
import typing as t
try:
from py_swagger_ui import swagger_ui_path
from py_swagger_ui import swagger_ui_path as default_template_dir
except ImportError:
swagger_ui_path = None
default_template_dir = None
NO_UI_MSG = """The swagger_ui directory could not be found.
Please install connexion with extra install: pip install connexion[swagger-ui]
@@ -18,131 +18,82 @@ NO_UI_MSG = """The swagger_ui directory could not be found.
logger = logging.getLogger("connexion.options")
@dataclasses.dataclass
class SwaggerUIOptions:
"""Options to configure the Swagger UI.
:param serve_spec: Whether to serve the Swagger / OpenAPI Specification
:param spec_path: Where to serve the Swagger / OpenAPI Specification
:param swagger_ui: Whether to serve the Swagger UI
:param swagger_ui_path: Where to serve the Swagger UI
:param swagger_ui_config: Options to configure the Swagger UI. See
https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration
for an overview of the available options.
:param swagger_ui_template_dir: Directory with static files to use to serve Swagger UI
:param swagger_ui_template_arguments: Arguments passed to the Swagger UI template. Useful
when providing your own template dir with additional template arguments.
"""
serve_spec: bool = True
spec_path: t.Optional[str] = None
swagger_ui: bool = True
swagger_ui_config: dict = dataclasses.field(default_factory=dict)
swagger_ui_path: str = "/ui"
swagger_ui_template_dir: t.Optional[str] = None
swagger_ui_template_arguments: dict = dataclasses.field(default_factory=dict)
class SwaggerUIConfig:
"""Class holding swagger UI specific options."""
def __init__(self, options=None, oas_version=(2,)):
self._options = {}
self.oas_version = oas_version
self.swagger_ui_local_path = swagger_ui_path
if self.oas_version >= (3, 0, 0):
self.openapi_spec_name = "/openapi.json"
else:
self.openapi_spec_name = "/swagger.json"
if options:
self._options.update(filter_values(options))
def extend(self, new_values=None):
# type: (Optional[dict]) -> SwaggerUIOptions
"""
Return a new instance of `ConnexionOptions` using as default the currently
defined options.
"""
if new_values is None:
new_values = {}
options = dict(self._options)
options.update(filter_values(new_values))
return SwaggerUIOptions(options, self.oas_version)
def as_dict(self):
return self._options
@property
def openapi_spec_available(self):
# type: () -> bool
"""
Whether to make available the OpenAPI Specification under
`openapi_spec_path`.
Default: True
"""
deprecated_option = self._options.get("swagger_json", True)
serve_spec = self._options.get("serve_spec", deprecated_option)
if "swagger_json" in self._options:
deprecation_warning = (
"The 'swagger_json' option is deprecated. "
"Please use 'serve_spec' instead"
)
logger.warning(deprecation_warning)
return serve_spec
@property
def openapi_console_ui_available(self):
# type: () -> bool
"""
Whether to make the OpenAPI Console UI available under the path
defined in `openapi_console_ui_path` option.
Default: True
"""
if (
self._options.get("swagger_ui", True)
and self.openapi_console_ui_from_dir is None
def __init__(
self,
options: t.Optional[SwaggerUIOptions] = None,
oas_version: t.Tuple[int, ...] = (2,),
):
if oas_version >= (3, 0, 0):
self.spec_path = "/openapi.json"
else:
self.spec_path = "/swagger.json"
self._options = options or SwaggerUIOptions()
@property
def openapi_spec_available(self) -> bool:
"""Whether to make the OpenAPI Specification available."""
return self._options.serve_spec
@property
def openapi_spec_path(self) -> str:
"""Path to host the Swagger UI."""
return self._options.spec_path or self.spec_path
@property
def swagger_ui_available(self) -> bool:
"""Whether to make the Swagger UI available."""
if self._options.swagger_ui and self.swagger_ui_template_dir is None:
logger.warning(NO_UI_MSG)
return False
return self._options.get("swagger_ui", True)
return self._options.swagger_ui
@property
def openapi_spec_path(self):
# type: () -> str
"""
Path to mount the OpenAPI Console UI and make it accessible via a browser.
Default: /openapi.json for openapi3, otherwise /swagger.json
"""
return self._options.get("openapi_spec_path", self.openapi_spec_name)
def swagger_ui_path(self) -> str:
"""Path to mount the Swagger UI and make it accessible via a browser."""
return self._options.swagger_ui_path
@property
def openapi_console_ui_path(self):
# type: () -> str
"""
Path to mount the OpenAPI Console UI and make it accessible via a browser.
Default: /ui
"""
return self._options.get("swagger_url", "/ui")
def swagger_ui_template_dir(self) -> str:
"""Directory with static files to use to serve Swagger UI."""
return self._options.swagger_ui_template_dir or default_template_dir
@property
def openapi_console_ui_from_dir(self):
# type: () -> str
"""
Custom OpenAPI Console UI directory from where Connexion will serve
the static files.
Default: Connexion's vendored version of the OpenAPI Console UI.
"""
return self._options.get("swagger_path", self.swagger_ui_local_path)
def swagger_ui_config(self) -> dict:
"""Options to configure the Swagger UI."""
return self._options.swagger_ui_config
@property
def openapi_console_ui_config(self):
# type: () -> dict
"""
Custom OpenAPI Console UI config.
Default: None
"""
return self._options.get("swagger_ui_config", None)
@property
def openapi_console_ui_index_template_variables(self):
# type: () -> dict
"""
Custom variables passed to the OpenAPI Console UI template.
Default: {}
"""
return self._options.get("swagger_ui_template_arguments", {})
def filter_values(dictionary):
# type: (dict) -> dict
"""
Remove `None` value entries in the dictionary.
:param dictionary:
:return:
"""
return {key: value for key, value in dictionary.items() if value is not None}
def swagger_ui_template_arguments(self) -> dict:
"""Arguments passed to the Swagger UI template."""
return self._options.swagger_ui_template_arguments

View File

@@ -64,8 +64,9 @@ Documentation
quickstart
middleware
cli
routing
swagger_ui
cli
request
response
security

View File

@@ -236,10 +236,12 @@ If you installed connexion using the :code:`swagger-ui` extra, a Swagger UI is a
API, providing interactive documentation. By default the UI is hosted at :code:`{base_path}/ui/`
where :code:`base_path`` is the base path of the API.
**https://localhost:{port}/{base_path}/ui/**
**https://{host}/{base_path}/ui/**
.. image:: images/swagger_ui.png
Check :doc:`swagger_ui` for information on how to configure the UI.
Full App class reference
------------------------

71
docs/swagger_ui.rst Normal file
View File

@@ -0,0 +1,71 @@
The Swagger UI
==============
If you installed connexion using the :code:`swagger-ui` extra, a Swagger UI is available for each
API, providing interactive documentation. By default the UI is hosted at :code:`{base_path}/ui/`
where :code:`base_path`` is the base path of the API.
**https://{host}/{base_path}/ui/**
.. image:: images/swagger_ui.png
Configuring the Swagger UI
--------------------------
You can change this path through the ``swagger_ui_options`` argument, either whe instantiating
your application, or when adding your api:
.. tab-set::
.. tab-item:: AsyncApp
:sync: AsyncApp
.. code-block:: python
:caption: **app.py**
from connexion import AsyncApp
from connexion.options import SwaggerUIOptions
options = SwaggerUIOptions(swagger_ui_path="/docs")
app = AsyncApp(__name__, swagger_ui_options=options)
app.add_api("openapi.yaml", swagger_ui_options=options)
.. tab-item:: FlaskApp
:sync: FlaskApp
.. code-block:: python
:caption: **app.py**
from connexion import FlaskApp
from connexion.options import SwaggerUIOptions
options = SwaggerUIOptions(swagger_ui_path="/docs")
app = FlaskApp(__name__, swagger_ui_options=options)
app.add_api("openapi.yaml", swagger_ui_options=options)
.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware
.. code-block:: python
:caption: **app.py**
from asgi_framework import App
from connexion import ConnexionMiddleware
from connexion.options import SwaggerUIOptions
options = SwaggerUIOptions(swagger_ui_path="/docs")
app = App(__name__)
app = ConnexionMiddleware(app, swagger_ui_options=options)
app.add_api("openapi.yaml", swagger_ui_options=options):
For a description of all available options, check the :class:`.SwaggerUIOptions`
class.
.. dropdown:: View a detailed reference of the :code:`SwaggerUIOptions` class
:icon: eye
.. autoclass:: connexion.options.SwaggerUIOptions

View File

@@ -105,7 +105,9 @@ should work, connexion comes with ``uvicorn`` as an extra:
Smaller breaking changes
------------------------
* The ``options`` argument has been renamed to ``swagger_ui_options``
* The ``options`` argument has been renamed to ``swagger_ui_options`` and now takes an instance
of the :class:`.SwaggerUIOptions`. The naming of the options themselves have been changed to
better represent their meaning.
* The ``uri_parser_class`` is now passed to the ``App`` or its ``add_api()`` method directly
instead of via the ``options`` argument.
* The ``jsonifier`` is now passed to the ``App`` or its ``add_api()`` method instead of setting it

View File

@@ -8,6 +8,7 @@ from connexion.exceptions import InvalidSpecification
from connexion.http_facts import METHODS
from connexion.json_schema import ExtendedSafeLoader
from connexion.middleware.abstract import AbstractRoutingAPI
from connexion.options import SwaggerUIOptions
from conftest import TEST_FOLDER, build_app_from_fixture
@@ -57,7 +58,7 @@ def test_swagger_ui(simple_api_spec_dir, spec):
def test_swagger_ui_with_config(simple_api_spec_dir, spec):
swagger_ui_config = {"displayOperationId": True}
swagger_ui_options = {"swagger_ui_config": swagger_ui_config}
swagger_ui_options = SwaggerUIOptions(swagger_ui_config=swagger_ui_config)
app = App(
__name__,
specification_dir=simple_api_spec_dir,
@@ -72,7 +73,7 @@ def test_swagger_ui_with_config(simple_api_spec_dir, spec):
def test_no_swagger_ui(simple_api_spec_dir, spec):
swagger_ui_options = {"swagger_ui": False}
swagger_ui_options = SwaggerUIOptions(swagger_ui=False)
app = App(
__name__,
specification_dir=simple_api_spec_dir,
@@ -85,7 +86,7 @@ def test_no_swagger_ui(simple_api_spec_dir, spec):
assert swagger_ui.status_code == 404
app2 = App(__name__, specification_dir=simple_api_spec_dir)
app2.add_api(spec, swagger_ui_options={"swagger_ui": False})
app2.add_api(spec, swagger_ui_options=SwaggerUIOptions(swagger_ui=False))
app2_client = app2.test_client()
swagger_ui2 = app2_client.get("/v1.0/ui/")
assert swagger_ui2.status_code == 404
@@ -94,7 +95,7 @@ def test_no_swagger_ui(simple_api_spec_dir, spec):
def test_swagger_ui_config_json(simple_api_spec_dir, spec):
"""Verify the swagger-ui-config.json file is returned for swagger_ui_config option passed to app."""
swagger_ui_config = {"displayOperationId": True}
swagger_ui_options = {"swagger_ui_config": swagger_ui_config}
swagger_ui_options = SwaggerUIOptions(swagger_ui_config=swagger_ui_config)
app = App(
__name__,
specification_dir=simple_api_spec_dir,
@@ -142,7 +143,7 @@ def test_swagger_yaml_app(simple_api_spec_dir, spec):
def test_no_swagger_json_app(simple_api_spec_dir, spec):
"""Verify the spec json file is not returned when set to False when creating app."""
swagger_ui_options = {"serve_spec": False}
swagger_ui_options = SwaggerUIOptions(serve_spec=False)
app = App(
__name__,
specification_dir=simple_api_spec_dir,
@@ -193,7 +194,7 @@ def test_swagger_json_api(simple_api_spec_dir, spec):
def test_no_swagger_json_api(simple_api_spec_dir, spec):
"""Verify the spec json file is not returned when set to False when adding api."""
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec, swagger_ui_options={"serve_spec": False})
app.add_api(spec, swagger_ui_options=SwaggerUIOptions(serve_spec=False))
app_client = app.test_client()
url = "/v1.0/{spec}".format(spec=spec.replace("yaml", "json"))

View File

@@ -1,7 +1,6 @@
import logging
from unittest.mock import MagicMock
import importlib_metadata
import pytest
from click.testing import CliRunner
from connexion.cli import main
@@ -9,6 +8,11 @@ from connexion.exceptions import ResolverError
from conftest import FIXTURES_FOLDER
try:
import importlib_metadata
except ImportError:
import importlib.metadata as importlib_metadata
@pytest.fixture(scope="function")
def mock_app_run(app_class, monkeypatch):