Skip to content

Commit

Permalink
refactor: openapi router (#2893)
Browse files Browse the repository at this point in the history
* refactor: openapi router

This PR refactors the way that we support multiple UIs for OpenAPI.

We add `litestar.openapi.plugins` where `OpenAPIRenderPlugin` is defined, and implementations of that plugin for the frameworks we currently support.

We add `OpenAPIConfig.render_plugins` config option, where a user can explicitly declare a set of plugins for UIs they wish to support.

If a user declares a sub-class of `OpenAPIController` at `OpenAPIConfig.openapi_controller`, then existing behavior is preserved exactly.

However, if no controller is explicitly declared, we invoke the new router-based approach, which should behave identically to the controller based approach (i.e., respect `enabled_endpoints` and `root_schema_site`).

Closes #2541

* docs: start of documentation re-write.

- creates an indexed directory for openapi
- removes the controller docs
- start of docs for plugins

* refactor: move JsonRenderPlugin into private ns

We add the json plugin, and have hardcoded refs to the path that it serves, so best not to make this public API just yet.

* docs: reference docs for plugins

* Revert "refactor: move JsonRenderPlugin into private ns"

This reverts commit 60719aa.

* docs: JsonRenderPlugin undocumented.

* docs: continue plugin docs

* test: run tests for both plugin and controller

Modifies tests where appropriate to run on both the plugin-based approach and the controller based approach.

* Implement default endpoint selection logic.

* Deprecation of OpenAPIController configs

* docs: swagger oauth examples

* Update docs/usage/openapi/ui_plugins.rst

* Update docs/usage/openapi/ui_plugins.rst

* fix: linting

* refactor: don't rely on DI for openapi schema in plugin handler.

* fix(test): there's an extra schema route to serve 404s.

* fix(docs): docstring indent

* fix(lint): remove redundant return

* refactor: plugins receive style tag instead of tag content.

* feat: allow openapi router to be handed to openapi config.

Allows for customization, such as adding guards, middleware, other routes, etc.

* feat: add `scalar` schema ui (#2906)

* Update litestar/openapi/plugins.py

Co-authored-by: Jacob Coffee <[email protected]>

* Update litestar/openapi/plugins.py

Co-authored-by: Jacob Coffee <[email protected]>

* Update litestar/openapi/plugins.py

Co-authored-by: Jacob Coffee <[email protected]>

* Update litestar/openapi/config.py

Co-authored-by: Jacob Coffee <[email protected]>

* Update litestar/openapi/config.py

Co-authored-by: Jacob Coffee <[email protected]>

* fix: update deprecation version

* fix: use GH repo for scalar links

* fix: update default scalar version

* fix: scalar plugin style attribute render.

Plugins expect that the style value is already wrapped in `<style>` tags.

* fix: serve default static files via jsdeliver

* fix: docstring syntax

* fix: removes custom repr

Can always add if there's a need for it, but we aren't using it.

* docs: another pass

* fix: style

* fix: test for updated build openapi plugin example

* fix: absolute paths for openapi.json

Resolves #3047 for the openapi router case.

* refactor: simplify scalar plugin.

* fix: linting

* Update litestar/_openapi/plugin.py

* refactor: test app to use scalar ui plugin

* fix: plugin customization example

Version arg is ignored if `js_url` is provided.

* fix: remove unnecessary kwargs

Removes passing default values to plugin kwargs in examples.

* fix: grammar error

* feat: make OpenAPIRenderPlugin an ABC

Abstract on `render()` method.

* fix: correct referenced lines

Referenced LoC in example had drifted.

* fix: more small docs corrections

* chore: remove dup spec of enabled endpoints.

* fix: simplify test.

---------

Co-authored-by: Jacob Coffee <[email protected]>
  • Loading branch information
2 people authored and provinzkraut committed Mar 17, 2024
1 parent bf8b33f commit c059899
Show file tree
Hide file tree
Showing 49 changed files with 2,372 additions and 522 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ app = Litestar(route_handlers=[hello_world])
[msgspec](https://github.com/jcrist/msgspec) and [attrs](https://www.attrs.org/en/stable/)
- Layered parameter declaration
- [Automatic API documentation with](#redoc-swagger-ui-and-stoplight-elements-api-documentation):
- [Scalar](https://github.com/scalar/scalar/)
- [RapiDoc](https://github.com/rapi-doc/RapiDoc)
- [Redoc](https://github.com/Redocly/redoc)
- [Stoplight Elements](https://github.com/stoplightio/elements)
Expand Down
1 change: 1 addition & 0 deletions docs/PYPI_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ app = Litestar(route_handlers=[hello_world])
[msgspec](https://github.com/jcrist/msgspec) and [attrs](https://www.attrs.org/en/stable/)
- Layered parameter declaration
- [Automatic API documentation with](#redoc-swagger-ui-and-stoplight-elements-api-documentation):
- [Scalar](https://github.com/scalar/scalar/)
- [RapiDoc](https://github.com/rapi-doc/RapiDoc)
- [Redoc](https://github.com/Redocly/redoc)
- [Stoplight Elements](https://github.com/stoplightio/elements)
Expand Down
20 changes: 20 additions & 0 deletions docs/examples/openapi/customize_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig


@get("/")
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="My API",
description="This is the description of my API",
version="0.1.0",
path="/docs",
),
)
Empty file.
51 changes: 51 additions & 0 deletions docs/examples/openapi/plugins/custom_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from collections.abc import Sequence
from typing import Any

from litestar.connection import Request
from litestar.openapi.plugins import OpenAPIRenderPlugin


class ScalarRenderPlugin(OpenAPIRenderPlugin):
def __init__(
self,
*,
version: str = "1.19.5",
js_url: str | None = None,
css_url: str | None = None,
path: str | Sequence[str] = "/scalar",
**kwargs: Any,
) -> None:
self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/@scalar/api-reference@{version}"
self.css_url = css_url
super().__init__(path=path, **kwargs)

def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes:
style_sheet_link = f'<link rel="stylesheet" type="text/css" href="{self.css_url}">' if self.css_url else ""
head = f"""
<head>
<title>{openapi_schema["info"]["title"]}</title>
{self.style}
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
{self.favicon}
{style_sheet_link}
</head>
"""

body = f"""
<script
id="api-reference"
data-url="openapi.json">
</script>
<script src="{self.js_url}" crossorigin></script>
"""

return f"""
<!DOCTYPE html>
<html>
{head}
{body}
</html>
""".encode()
3 changes: 3 additions & 0 deletions docs/examples/openapi/plugins/rapidoc_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from litestar.openapi.plugins import RapidocRenderPlugin

rapidoc_plugin = RapidocRenderPlugin(version="9.3.4", path="/rapidoc")
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/rapidoc_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import RapidocRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[RapidocRenderPlugin()],
),
)
23 changes: 23 additions & 0 deletions docs/examples/openapi/plugins/receive_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from litestar import get
from litestar.enums import MediaType
from litestar.openapi.plugins import OpenAPIRenderPlugin

if TYPE_CHECKING:
from litestar.connection import Request
from litestar.router import Router


class MyOpenAPIPlugin(OpenAPIRenderPlugin):
def render(self, request: Request, openapi_schema: dict[str, str]) -> bytes:
return b"<html>My UI of Choice!</html>"

def receive_router(self, router: Router) -> None:
@get("/something", media_type=MediaType.TEXT)
def something() -> str:
return "Something"

router.register(something)
3 changes: 3 additions & 0 deletions docs/examples/openapi/plugins/redoc_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from litestar.openapi.plugins import RedocRenderPlugin

redoc_plugin = RedocRenderPlugin(version="next", google_fonts=True, path="/redoc")
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/redoc_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import RedocRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[RedocRenderPlugin()],
),
)
3 changes: 3 additions & 0 deletions docs/examples/openapi/plugins/scalar_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from litestar.openapi.plugins import ScalarRenderPlugin

scalar_plugin = ScalarRenderPlugin(version="1.19.5", path="/scalar")
7 changes: 7 additions & 0 deletions docs/examples/openapi/plugins/scalar_customized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from litestar.openapi.plugins import ScalarRenderPlugin

scalar_plugin = ScalarRenderPlugin(
js_url="https://example.com/my-custom-scalar.js",
css_url="https://example.com/my-custom-scalar.css",
path="/scalar",
)
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/scalar_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import ScalarRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of Litestar with Scalar OpenAPI docs",
version="0.0.1",
render_plugins=[ScalarRenderPlugin()],
),
)
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/serving_multiple_uis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import RapidocRenderPlugin, SwaggerRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[RapidocRenderPlugin(), SwaggerRenderPlugin()],
),
)
3 changes: 3 additions & 0 deletions docs/examples/openapi/plugins/stoplight_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from litestar.openapi.plugins import StoplightRenderPlugin

stoplight_plugin = StoplightRenderPlugin(version="7.7.18", path="/elements")
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/stoplight_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import StoplightRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[StoplightRenderPlugin()],
),
)
3 changes: 3 additions & 0 deletions docs/examples/openapi/plugins/swagger_ui_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from litestar.openapi.plugins import SwaggerRenderPlugin

swagger_plugin = SwaggerRenderPlugin(version="5.1.3", path="/swagger")
32 changes: 32 additions & 0 deletions docs/examples/openapi/plugins/swagger_ui_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import SwaggerRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[
SwaggerRenderPlugin(
init_oauth={
"clientId": "your-client-id",
"appName": "your-app-name",
"scopeSeparator": " ",
"scopes": "openid profile",
"useBasicAuthenticationWithAccessCodeGrant": True,
"usePkceWithAuthorizationCodeGrant": True,
}
)
],
),
)
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/swagger_ui_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import SwaggerRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[SwaggerRenderPlugin()],
),
)
21 changes: 21 additions & 0 deletions docs/examples/openapi/plugins/yaml_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Dict

from litestar import Litestar, get
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.plugins import YamlRenderPlugin


@get("/", sync_to_thread=False)
def hello_world() -> Dict[str, str]:
return {"message": "Hello World"}


app = Litestar(
route_handlers=[hello_world],
openapi_config=OpenAPIConfig(
title="Litestar Example",
description="Example of litestar",
version="0.0.1",
render_plugins=[YamlRenderPlugin()],
),
)
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Litestar is a powerful, flexible, highly performant, and opinionated ASGI framew

The Litestar framework supports :doc:`/usage/plugins/index`, ships
with :doc:`dependency injection </usage/dependency-injection>`, :doc:`security primitives </usage/security/index>`,
:doc:`OpenAPI schema generation </usage/openapi>`, `MessagePack <https://msgpack.org/>`_,
:doc:`OpenAPI schema generation </usage/openapi/index>`, `MessagePack <https://msgpack.org/>`_,
:doc:`middlewares </usage/middleware/index>`, a great :doc:`CLI </usage/cli>` experience, and much more.

Installation
Expand Down Expand Up @@ -144,7 +144,7 @@ A huge thank you to our current sponsors:

<div style="display: flex; justify-content: center; align-items: center; gap: 8px;">
<div>
<a href="https://scalar.com">
<a href="https://github.com/scalar/scalar">
<img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/sponsors/scalar.svg" alt="Scalar.com" style="border-radius: 10px; width: auto; max-height: 150px;"/>
</a>
<p style="text-align: center;">Scalar.com</p>
Expand Down
1 change: 1 addition & 0 deletions docs/reference/openapi/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ openapi
:maxdepth: 1

openapi
plugins
spec
5 changes: 5 additions & 0 deletions docs/reference/openapi/plugins.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
plugins
=======

.. automodule:: litestar.openapi.plugins
:members:
2 changes: 1 addition & 1 deletion docs/usage/applications.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Parameters that support layering are:
* :ref:`etag <usage/responses:etag>`
* :doc:`exception_handlers </usage/exceptions>`
* :doc:`guards </usage/security/guards>`
* :ref:`include_in_schema <usage/openapi:configuring schema generation on a route handler>`
* :ref:`include_in_schema <usage/openapi/schema_generation:configuring schema generation on a route handler>`
* :doc:`middleware </usage/middleware/index>`
* :ref:`opt <handler_opts>`
* :ref:`response_class <usage/responses:custom responses>`
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Usage
logging
metrics/index
middleware/index
openapi
openapi/index
plugins/index
responses
security/index
Expand Down
22 changes: 22 additions & 0 deletions docs/usage/openapi/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
OpenAPI
=======

Litestar has first class OpenAPI support offering the following features:

- Automatic `OpenAPI 3.1.0 Schema <https://spec.openapis.org/oas/v3.1.0>`_ generation, which is available as both YAML
and JSON.
- Builtin support for static documentation site generation using several different libraries.
- Simple configuration using pydantic based classes.


Litestar includes a complete implementation of the `latest version of the OpenAPI specification <https://spec.openapis.org/oas/latest.html>`_
using Python dataclasses. This implementation is used as a basis for generating OpenAPI specs, supporting builtins including
``dataclasses`` and ``TypedDict``, as well as Pydantic models and any 3rd party entities for which a plugin is implemented.

This is also highly configurable - and users can customize the OpenAPI spec in a variety of ways - ranging from passing
configuration globally, to settings specific kwargs on route handler decorators.

.. toctree::

schema_generation
ui_plugins
Loading

0 comments on commit c059899

Please sign in to comment.