From b2adb0d083fb462ac7bcfb86a012037abe856d8c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 19 Oct 2024 12:55:30 +0300 Subject: [PATCH] docs: refactor "Custom Middleware" guide (#3833) --- docs/examples/middleware/base.py | 34 ++++------- docs/usage/middleware/creating-middleware.rst | 60 +++++++++---------- docs/usage/middleware/using-middleware.rst | 2 + 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/docs/examples/middleware/base.py b/docs/examples/middleware/base.py index adc8bcf4f0..a2a834e7d7 100644 --- a/docs/examples/middleware/base.py +++ b/docs/examples/middleware/base.py @@ -1,32 +1,24 @@ -from time import time -from typing import TYPE_CHECKING, Dict +import time +from typing import Dict -from litestar import Litestar, get, websocket +from litestar import Litestar, WebSocket, get, websocket from litestar.datastructures import MutableScopeHeaders from litestar.enums import ScopeType from litestar.middleware import AbstractMiddleware - -if TYPE_CHECKING: - from litestar import WebSocket - from litestar.types import Message, Receive, Scope, Send +from litestar.types import Message, Receive, Scope, Send class MyMiddleware(AbstractMiddleware): scopes = {ScopeType.HTTP} exclude = ["first_path", "second_path"] - exclude_opt_key = "exclude_from_middleware" + exclude_opt_key = "exclude_from_my_middleware" - async def __call__( - self, - scope: "Scope", - receive: "Receive", - send: "Send", - ) -> None: - start_time = time() + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + start_time = time.monotonic() async def send_wrapper(message: "Message") -> None: if message["type"] == "http.response.start": - process_time = time() - start_time + process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) @@ -35,12 +27,12 @@ async def send_wrapper(message: "Message") -> None: @websocket("/my-websocket") -async def websocket_handler(socket: "WebSocket") -> None: +async def websocket_handler(socket: WebSocket) -> None: """ Websocket handler - is excluded because the middleware scopes includes 'ScopeType.HTTP' """ await socket.accept() - await socket.send_json({"hello websocket"}) + await socket.send_json({"hello": "websocket"}) await socket.close() @@ -56,10 +48,10 @@ def second_handler() -> Dict[str, str]: return {"hello": "second"} -@get("/third_path", exclude_from_middleware=True, sync_to_thread=False) +@get("/third_path", exclude_from_my_middleware=True, sync_to_thread=False) def third_handler() -> Dict[str, str]: - """Handler is excluded due to the opt key 'exclude_from_middleware' matching the middleware 'exclude_opt_key'.""" - return {"hello": "second"} + """Handler is excluded due to the opt key 'exclude_from_my_middleware' matching the middleware 'exclude_opt_key'.""" + return {"hello": "third"} @get("/greet", sync_to_thread=False) diff --git a/docs/usage/middleware/creating-middleware.rst b/docs/usage/middleware/creating-middleware.rst index 74e8e40a39..d06cfff23a 100644 --- a/docs/usage/middleware/creating-middleware.rst +++ b/docs/usage/middleware/creating-middleware.rst @@ -2,9 +2,9 @@ Creating Middleware =================== -As mentioned in :doc:`using middleware `, a middleware in Litestar +As mentioned in :ref:`using middleware `, a middleware in Litestar is **any callable** that takes a kwarg called ``app``, which is the next ASGI handler, i.e. an -:class:`ASGIApp `, and returns an ``ASGIApp``. +:class:`~litestar.types.ASGIApp`, and returns an ``ASGIApp``. The example previously given was using a factory function, i.e.: @@ -22,14 +22,14 @@ The example previously given was using a factory function, i.e.: return my_middleware While using functions is a perfectly viable approach, you can also use classes to do the same. See the next sections on -two base classes you can use for this purpose - the :class:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` , -which gives a bare-bones type, or the :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` that offers a +two base classes you can use for this purpose - the :class:`~litestar.middleware.base.MiddlewareProtocol` , +which gives a bare-bones type, or the :class:`~litestar.middleware.base.AbstractMiddleware` that offers a base class with some built in functionality. Using MiddlewareProtocol ------------------------ -The :class:`MiddlewareProtocol ` class is a +The :class:`~litestar.middleware.base.MiddlewareProtocol` class is a `PEP 544 Protocol `_ that specifies the minimal implementation of a middleware as follows: @@ -50,7 +50,7 @@ this case, but rather the next middleware in the stack, which is also an ASGI ap The ``__call__`` method makes this class into a ``callable``, i.e. once instantiated this class acts like a function, that has the signature of an ASGI app: The three parameters, ``scope, receive, send`` are specified by `the ASGI specification `_, and their values originate with the ASGI -server (e.g. *uvicorn*\ ) used to run Litestar. +server (e.g. ``uvicorn``\ ) used to run Litestar. To use this protocol as a basis, simply subclass it - as you would any other class, and implement the two methods it specifies: @@ -67,20 +67,19 @@ specifies: class MyRequestLoggingMiddleware(MiddlewareProtocol): - def __init__(self, app: ASGIApp) -> None: - super().__init__(app) + def __init__(self, app: ASGIApp) -> None: # can have other parameters as well self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": request = Request(scope) - logger.info("%s - %s" % request.method, request.url) + logger.info("Got request: %s - %s", request.method, request.url) await self.app(scope, receive, send) .. important:: Although ``scope`` is used to create an instance of request by passing it to the - :class:`Request <.connection.Request>` constructor, which makes it simpler to access because it does some parsing + :class:`~litestar.connection.Request` constructor, which makes it simpler to access because it does some parsing for you already, the actual source of truth remains ``scope`` - not the request. If you need to modify the data of the request you must modify the scope object, not any ephemeral request objects created as in the above. @@ -103,7 +102,6 @@ explore another example - redirecting the request to a different url from a midd class RedirectMiddleware(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: - super().__init__(app) self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: @@ -113,24 +111,24 @@ explore another example - redirecting the request to a different url from a midd else: await self.app(scope, receive, send) -As you can see in the above, given some condition (``request.session`` being None) we create a -:class:`ASGIRedirectResponse ` and then await it. Otherwise, we await ``self.app`` +As you can see in the above, given some condition (``request.session`` being ``None``) we create a +:class:`~litestar.response.redirect.ASGIRedirectResponse` and then await it. Otherwise, we await ``self.app`` Modifying ASGI Requests and Responses using the MiddlewareProtocol ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. important:: - If you'd like to modify a :class:`Response <.response.Response>` object after it was created for a route + If you'd like to modify a :class:`~litestar.response.Response` object after it was created for a route handler function but before the actual response message is transmitted, the correct place to do this is using the special life-cycle hook called :ref:`after_request `. The instructions in this section are for how to modify the ASGI response message itself, which is a step further in the response process. -Using the :class:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` you can intercept and modifying both the +Using the :class:`~litestar.middleware.base.MiddlewareProtocol` you can intercept and modifying both the incoming and outgoing data in a request / response cycle by "wrapping" that respective ``receive`` and ``send`` ASGI functions. -To demonstrate this, lets say we want to append a header with a timestamp to all outgoing responses. We could achieve +To demonstrate this, let's say we want to append a header with a timestamp to all outgoing responses. We could achieve this by doing the following: .. code-block:: python @@ -150,11 +148,11 @@ this by doing the following: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": - start_time = time.time() + start_time = time.monotonic() async def send_wrapper(message: Message) -> None: if message["type"] == "http.response.start": - process_time = time.time() - start_time + process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) @@ -166,21 +164,17 @@ this by doing the following: Inheriting AbstractMiddleware ----------------------------- -Litestar offers an :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` class that can be extended to +Litestar offers an :class:`~litestar.middleware.base.AbstractMiddleware` class that can be extended to create middleware: .. code-block:: python - from typing import TYPE_CHECKING - from time import time + import time from litestar.enums import ScopeType from litestar.middleware import AbstractMiddleware from litestar.datastructures import MutableScopeHeaders - - - if TYPE_CHECKING: - from litestar.types import Message, Receive, Scope, Send + from litestar.types import Message, Receive, Scope, Send class MyMiddleware(AbstractMiddleware): @@ -188,12 +182,12 @@ create middleware: exclude = ["first_path", "second_path"] exclude_opt_key = "exclude_from_middleware" - async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: - start_time = time() + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + start_time = time.monotonic() async def send_wrapper(message: "Message") -> None: if message["type"] == "http.response.start": - process_time = time() - start_time + process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) @@ -204,11 +198,11 @@ The three class variables defined in the above example ``scopes``, ``exclude``, fine-tune for which routes and request types the middleware is called: -- The scopes variable is a set that can include either or both ``ScopeType.HTTP`` and ``ScopeType.WEBSOCKET`` , with the default being both. +- The scopes variable is a set that can include either or both : ``ScopeType.HTTP`` and ``ScopeType.WEBSOCKET`` , with the default being both. - ``exclude`` accepts either a single string or list of strings that are compiled into a regex against which the request's ``path`` is checked. -- ``exclude_opt_key`` is the key to use for in a route handler's ``opt`` dict for a boolean, whether to omit from the middleware. +- ``exclude_opt_key`` is the key to use for in a route handler's :class:`Router.opt ` dict for a boolean, whether to omit from the middleware. -Thus, in the following example, the middleware will only run against the route handler called ``not_excluded_handler``: +Thus, in the following example, the middleware will only run against the handler called ``not_excluded_handler`` for ``/greet`` route: .. literalinclude:: /examples/middleware/base.py :language: python @@ -222,8 +216,8 @@ Thus, in the following example, the middleware will only run against the route h Using DefineMiddleware to pass arguments ---------------------------------------- -Litestar offers a simple way to pass positional arguments (``*args``) and key-word arguments (``**kwargs``) to middleware -using the :class:`DefineMiddleware ` class. Let's extend +Litestar offers a simple way to pass positional arguments (``*args``) and keyword arguments (``**kwargs``) to middleware +using the :class:`~litestar.middleware.base.DefineMiddleware` class. Let's extend the factory function used in the examples above to take some args and kwargs and then use ``DefineMiddleware`` to pass these values to our middleware: diff --git a/docs/usage/middleware/using-middleware.rst b/docs/usage/middleware/using-middleware.rst index 2190676fc5..47456bfc27 100644 --- a/docs/usage/middleware/using-middleware.rst +++ b/docs/usage/middleware/using-middleware.rst @@ -1,3 +1,5 @@ +.. _using-middleware: + Using Middleware ================