From c01d68bb6d5efa5f91b4062fd5e9a43f9020092f Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Tue, 2 Apr 2024 21:01:39 -0500 Subject: [PATCH] docs: update `docs/usage/routing/*` (#3301) * docs: update usage/routing/* * ci: add ``sphinx-paramlinks`` ci: add ``sphinx-paramlinks`` * Update docs/usage/routing/handlers.rst Co-authored-by: Peter Schutt * Update docs/usage/routing/handlers.rst Co-authored-by: Peter Schutt * Update docs/usage/routing/handlers.rst Co-authored-by: Peter Schutt * Update docs/usage/routing/handlers.rst Co-authored-by: Peter Schutt * Update docs/usage/routing/handlers.rst Co-authored-by: Peter Schutt * Update docs/usage/routing/handlers.rst Co-authored-by: Peter Schutt * Update docs/usage/routing/overview.rst * Update docs/usage/routing/parameters.rst * docs(review): apply code review * docs(review): apply code review * docs(review): apply code review * docs(review): apply code review * docs(review): apply code review * docs(review): apply code review --------- Co-authored-by: Peter Schutt --- docs/conf.py | 1 + docs/usage/routing/handlers.rst | 544 +++++++++++++++--------------- docs/usage/routing/index.rst | 7 + docs/usage/routing/overview.rst | 253 +++++++------- docs/usage/routing/parameters.rst | 206 ++++++----- pyproject.toml | 19 +- 6 files changed, 522 insertions(+), 508 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ebad191eb9..a35403a248 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ "sphinx_copybutton", "sphinxcontrib.mermaid", "sphinx_click", + "sphinx_paramlinks", ] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] diff --git a/docs/usage/routing/handlers.rst b/docs/usage/routing/handlers.rst index f09a9c6015..78fc9f6e7d 100644 --- a/docs/usage/routing/handlers.rst +++ b/docs/usage/routing/handlers.rst @@ -2,320 +2,326 @@ Route handlers ============== Route handlers are the core of Litestar. They are constructed by decorating a function or class method with one of the -handler decorators exported from Litestar. +handler :term:`decorators ` exported from Litestar. For example: .. code-block:: python + :caption: Defining a route handler by decorating a function with the :class:`@get() <.handlers.get>` :term:`decorator` - from litestar import get + from litestar import get - @get("/") - def greet() -> str: + @get("/") + def greet() -> str: return "hello world" -In the above example, the decorator includes all the information required to define the endpoint operation for the -combination of the path ``"/"`` and the http verb ``GET``. In this case it will be a http response with a "Content-Type" -header of ``text/plain``. - -What the decorator does, is wrap the function or method within a class instance that inherits from -:class:`BaseRouteHandler <.handlers.base.BaseRouteHandler>`. These classes are optimized -descriptor classes that record all the data necessary for the given function or method - this includes a modelling of -the function signature, which allows for injection of kwargs and dependencies, as well as data pertinent to OpenAPI -spec generation. - +In the above example, the :term:`decorator` includes all the information required to define the endpoint operation for +the combination of the path ``"/"`` and the HTTP verb ``GET``. In this case it will be a HTTP response with a +``Content-Type`` header of ``text/plain``. .. include:: /admonitions/sync-to-thread-info.rst - Declaring paths --------------- -All route handler decorator accept an optional path argument. This argument can be declared as a kwarg using the ``path`` -key word: +All route handler :term:`decorators ` accept an optional path :term:`argument`. +This :term:`argument` can be declared as a :term:`kwarg ` using the +:paramref:`~.handlers.base.BaseRouteHandler.path` parameter: .. code-block:: python + :caption: Defining a route handler by passing the path as a keyword argument - from litestar import get + from litestar import get - @get(path="/some-path") - async def my_route_handler() -> None: ... + @get(path="/some-path") + async def my_route_handler() -> None: ... -It can also be passed as an argument without the key-word: +It can also be passed as an :term:`argument` without the keyword: .. code-block:: python + :caption: Defining a route handler but not using the keyword argument - from litestar import get + from litestar import get - @get("/some-path") - async def my_route_handler() -> None: ... + @get("/some-path") + async def my_route_handler() -> None: ... -And the value for this argument can be either a string path, as in the above examples, or a list of string paths: +And the value for this :term:`argument` can be either a string path, as in the above examples, or a :class:`list` of +:class:`string ` paths: .. code-block:: python + :caption: Defining a route handler with multiple paths - from litestar import get + from litestar import get - @get(["/some-path", "/some-other-path"]) - async def my_route_handler() -> None: ... + @get(["/some-path", "/some-other-path"]) + async def my_route_handler() -> None: ... -This is particularly useful when you want to have optional :ref:`path parameters `: +This is particularly useful when you want to have optional +:ref:`path parameters `: .. code-block:: python + :caption: Defining a route handler with a path that has an optional path parameter - from litestar import get + from litestar import get - @get( + @get( ["/some-path", "/some-path/{some_id:int}"], - ) - async def my_route_handler(some_id: int = 1) -> None: ... + ) + async def my_route_handler(some_id: int = 1) -> None: ... .. _handler-function-kwargs: "reserved" keyword arguments ---------------------------- -Route handler functions or methods access various data by declaring these as annotated function kwargs. The annotated -kwargs are inspected by Litestar and then injected into the request handler. +Route handler functions or methods access various data by declaring these as annotated function :term:`kwargs `. The annotated +:term:`kwargs ` are inspected by Litestar and then injected into the request handler. -The following sources can be accessed using annotated function kwargs: +The following sources can be accessed using annotated function :term:`kwargs `: - :ref:`path, query, header, and cookie parameters ` -- :doc:`/usage/requests` +- :doc:`requests ` - :doc:`injected dependencies ` -Additionally, you can specify the following special kwargs, what's called "reserved keywords" internally: +Additionally, you can specify the following special :term:`kwargs `, +(known as "reserved keywords"): - -* ``cookies``: injects the request :class:`cookies <.datastructures.cookie.Cookie>` as a parsed dictionary. -* ``headers``: injects the request headers as a parsed dictionary. -* ``query`` : injects the request ``query_params`` as a parsed dictionary. -* ``request``: injects the :class:`Request <.connection.Request>` instance. Available only for `http route handlers`_ -* ``scope`` : injects the ASGI scope dictionary. +* ``cookies``: injects the request :class:`cookies <.datastructures.cookie.Cookie>` as a parsed + :class:`dictionary `. +* ``headers``: injects the request headers as a parsed :class:`dictionary `. +* ``query`` : injects the request ``query_params`` as a parsed :class:`dictionary `. +* ``request``: injects the :class:`Request <.connection.Request>` instance. Available only for `HTTP route handlers`_ +* ``scope`` : injects the ASGI scope :class:`dictionary `. * ``socket``: injects the :class:`WebSocket <.connection.WebSocket>` instance. Available only for `websocket route handlers`_ * ``state`` : injects a copy of the application :class:`State <.datastructures.state.State>`. -* ``body`` : the raw request body. Available only for `http route handlers`_ +* ``body`` : the raw request body. Available only for `HTTP route handlers`_ -Note that if your parameters collide with any of the reserved keyword arguments above, you can :ref:`provide an alternative name `. +Note that if your parameters collide with any of the reserved :term:`keyword arguments ` above, you can +:ref:`provide an alternative name `. For example: .. code-block:: python + :caption: Providing an alternative name for a reserved keyword argument - from typing import Any, Dict - from litestar import Request, get - from litestar.datastructures import Headers, State + from typing import Any, Dict + from litestar import Request, get + from litestar.datastructures import Headers, State - @get(path="/") - async def my_request_handler( + @get(path="/") + async def my_request_handler( state: State, request: Request, headers: Dict[str, str], query: Dict[str, Any], cookies: Dict[str, Any], - ) -> None: ... - -.. tip:: + ) -> None: ... - You can define a custom typing for your application state and then use it as a type instead of just using the - State class from Litestar +.. tip:: You can define a custom typing for your application state and then use it as a type instead of just using the + :class:`~.datastructures.state.State` class from Litestar Type annotations ---------------- -Litestar enforces strict type annotations. Functions decorated by a route handler **must** have all their kwargs and -return value type annotated. If a type annotation is missing, an -:class:`ImproperlyConfiguredException ` will be raised during the +Litestar enforces strict :term:`type annotations `. +Functions decorated by a route handler **must** have all their :term:`arguments ` and return +value type annotated. + +If a type annotation is missing, an :exc:`~.exceptions.ImproperlyConfiguredException` will be raised during the application boot-up process. There are several reasons for why this limitation is enforced: - -#. to ensure best practices -#. to ensure consistent OpenAPI schema generation -#. to allow Litestar to compute during the application bootstrap all the kwargs required by a function - +#. To ensure best practices +#. To ensure consistent OpenAPI schema generation +#. To allow Litestar to compute the :term:`arguments ` required by a function during application bootstrap HTTP route handlers ------------------- -The most commonly used route handlers are those that handle http requests and responses. These route handlers all -inherit from the class :class:`HTTPRouteHandler `, which -is aliased as the decorator called :func:`route `: +The most commonly used route handlers are those that handle HTTP requests and responses. +These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` class, which is aliased as the +:term:`decorator` called :func:`~.handlers.route`: .. code-block:: python + :caption: Defining a route handler by decorating a function with the :class:`@route() <.handlers.route>` + :term:`decorator` - from litestar import HttpMethod, route + from litestar import HttpMethod, route - @route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) - async def my_endpoint() -> None: ... + @route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) + async def my_endpoint() -> None: ... -As mentioned above, ``route`` does is merely an alias for ``HTTPRouteHandler``\ , thus the below code is equivalent to the one -above: +As mentioned above, :func:`@route() <.handlers.route>` is merely an alias for ``HTTPRouteHandler``, +thus the below code is equivalent to the one above: .. code-block:: python + :caption: Defining a route handler by decorating a function with the + :class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>` class - from litestar import HttpMethod - from litestar.handlers.http_handlers import HTTPRouteHandler - + from litestar import HttpMethod + from litestar.handlers.http_handlers import HTTPRouteHandler - @HTTPRouteHandler(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) - async def my_endpoint() -> None: ... + @HTTPRouteHandler(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) + async def my_endpoint() -> None: ... -Semantic handler decorators -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Semantic handler :term:`decorators ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Litestar also includes "semantic" decorators, that is, decorators the pre-set the ``http_method`` kwarg to a specific HTTP -verb, which correlates with their name: +Litestar also includes "semantic" :term:`decorators `, that is, :term:`decorators ` the pre-set +the :paramref:`~litestar.handlers.HTTPRouteHandler.http_method` :term:`kwarg ` to a specific HTTP verb, +which correlates with their name: +* :func:`@delete() <.handlers.delete>` +* :func:`@get() <.handlers.get>` +* :func:`@head() <.handlers.head>` +* :func:`@patch() <.handlers.patch>` +* :func:`@post() <.handlers.post>` +* :func:`@put() <.handlers.put>` -* :func:`delete ` -* :func:`get ` -* :func:`head ` -* :func:`patch ` -* :func:`post ` -* :func:`put ` +These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you cannot configure the +:paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg `: -These are used exactly like ``route`` with the sole exception that you cannot configure the ``http_method`` kwarg: +.. dropdown:: Click to see the predefined route handlers -.. code-block:: python - - from litestar import delete, get, patch, post, put, head - from litestar.dto import DTOConfig, DTOData - from litestar.contrib.pydantic import PydanticDTO + .. code-block:: python + :caption: Predefined :term:`decorators ` for HTTP route handlers - from pydantic import BaseModel + from litestar import delete, get, patch, post, put, head + from litestar.dto import DTOConfig, DTOData + from litestar.contrib.pydantic import PydanticDTO + from pydantic import BaseModel - class Resource(BaseModel): ... + class Resource(BaseModel): ... - class PartialResourceDTO(PydanticDTO[Resource]): - config = DTOConfig(partial=True) + class PartialResourceDTO(PydanticDTO[Resource]): + config = DTOConfig(partial=True) - @get(path="/resources") - async def list_resources() -> list[Resource]: ... + @get(path="/resources") + async def list_resources() -> list[Resource]: ... - @post(path="/resources") - async def create_resource(data: Resource) -> Resource: ... + @post(path="/resources") + async def create_resource(data: Resource) -> Resource: ... - @get(path="/resources/{pk:int}") - async def retrieve_resource(pk: int) -> Resource: ... + @get(path="/resources/{pk:int}") + async def retrieve_resource(pk: int) -> Resource: ... - @head(path="/resources/{pk:int}") - async def retrieve_resource_head(pk: int) -> None: ... + @head(path="/resources/{pk:int}") + async def retrieve_resource_head(pk: int) -> None: ... - @put(path="/resources/{pk:int}") - async def update_resource(data: Resource, pk: int) -> Resource: ... + @put(path="/resources/{pk:int}") + async def update_resource(data: Resource, pk: int) -> Resource: ... - @patch(path="/resources/{pk:int}", dto=PartialResourceDTO) - async def partially_update_resource( - data: DTOData[PartialResourceDTO], pk: int - ) -> Resource: ... + @patch(path="/resources/{pk:int}", dto=PartialResourceDTO) + async def partially_update_resource( + data: DTOData[PartialResourceDTO], pk: int + ) -> Resource: ... - @delete(path="/resources/{pk:int}") - async def delete_resource(pk: int) -> None: ... -Although these decorators are merely subclasses of :class:`HTTPRouteHandler ` -that pre-set the ``http_method``, using *get*, *patch*, *put*, *delete*, or *post* instead of *route* makes the -code clearer and simpler. + @delete(path="/resources/{pk:int}") + async def delete_resource(pk: int) -> None: ... -Furthermore, in the OpenAPI specification each unique combination of http verb (e.g. "GET", "POST", etc.) and path is -regarded as a distinct `operation `_\ , and each operation -should be distinguished by a unique ``operation_id`` and optimally also have a ``summary`` and ``description`` sections. +Although these :term:`decorators ` are merely subclasses of :class:`~.handlers.HTTPRouteHandler` that pre-set +the :paramref:`~.handlers.HTTPRouteHandler.http_method`, using :func:`@get() <.handlers.get>`, +:func:`@patch() <.handlers.patch>`, :func:`@put() <.handlers.put>`, :func:`@delete() <.handlers.delete>`, or +:func:`@post() <.handlers.post>` instead of :func:`@route() <.handlers.route>` makes the code clearer and simpler. -As such, using the ``route`` decorator is discouraged. Instead, the preferred pattern is to share code using secondary -class methods or by abstracting code to reusable functions. +Furthermore, in the OpenAPI specification each unique combination of HTTP verb (e.g. ``GET``, ``POST``, etc.) and path +is regarded as a distinct `operation `_\ , and each +operation should be distinguished by a unique :paramref:`~.handlers.HTTPRouteHandler.operation_id` and optimally +also have a :paramref:`~.handlers.HTTPRouteHandler.summary` and +:paramref:`~.handlers.HTTPRouteHandler.description` sections. +As such, using the :func:`@route() <.handlers.route>` :term:`decorator` is discouraged. +Instead, the preferred pattern is to share code using secondary class methods or by abstracting code to reusable +functions. Websocket route handlers ------------------------ -A WebSocket connection can be handled with a :func:`websocket ` route handler. +A WebSocket connection can be handled with a :func:`@websocket() <.handlers.WebsocketRouteHandler>` route handler. -.. note:: - The websocket handler is a low level approach, requiring to handle the socket directly, +.. note:: The websocket handler is a low level approach, requiring to handle the socket directly, and dealing with keeping it open, exceptions, client disconnects, and content negotiation. For a more high level approach to handling WebSockets, see :doc:`/usage/websockets` .. code-block:: python + :caption: Using the :func:`@websocket() <.handlers.WebsocketRouteHandler>` route handler :term:`decorator` - from litestar import WebSocket, websocket + from litestar import WebSocket, websocket - @websocket(path="/socket") - async def my_websocket_handler(socket: WebSocket) -> None: + @websocket(path="/socket") + async def my_websocket_handler(socket: WebSocket) -> None: await socket.accept() await socket.send_json({...}) await socket.close() -The ``websocket`` decorator is an alias of the class -:class:`WebsocketRouteHandler <.handlers.WebsocketRouteHandler>`. Thus, the below -code is equivalent to the one above: +The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is an alias of the +:class:`~.handlers.WebsocketRouteHandler` class. Thus, the below code is equivalent to the one above: .. code-block:: python + :caption: Using the :class:`~.handlers.WebsocketRouteHandler` class directly - from litestar import WebSocket - from litestar.handlers.websocket_handlers import WebsocketRouteHandler + from litestar import WebSocket + from litestar.handlers.websocket_handlers import WebsocketRouteHandler - @WebsocketRouteHandler(path="/socket") - async def my_websocket_handler(socket: WebSocket) -> None: + @WebsocketRouteHandler(path="/socket") + async def my_websocket_handler(socket: WebSocket) -> None: await socket.accept() await socket.send_json({...}) await socket.close() In difference to HTTP routes handlers, websocket handlers have the following requirements: +#. They **must** declare a ``socket`` :term:`kwarg `. +#. They **must** have a return :term:`annotation` of ``None``. +#. They **must** be :ref:`async functions `. -#. they **must** declare a ``socket`` kwarg. -#. they **must** have a return annotation of ``None``. -#. they **must** be async functions. - -These requirements are enforced using inspection, and if any of them is unfulfilled an informative exception will be raised. +These requirements are enforced using inspection, and if any of them is unfulfilled an informative exception +will be raised. -.. note:: +OpenAPI currently does not support websockets. As such no schema will be generated for these route handlers. - OpenAPI currently does not support websockets. As such no schema will be generated for these route handlers. - - -.. seealso:: - - * :class:`WebsocketRouteHandler ` +.. seealso:: * :class:`~.handlers.WebsocketRouteHandler` * :doc:`/usage/websockets` - ASGI route handlers ------------------- -If you need to write your own ASGI application, you can do so using the :func:`asgi ` decorator: +If you need to write your own ASGI application, you can do so using the :func:`@asgi() <.handlers.asgi>` :term:`decorator`: .. code-block:: python + :caption: Using the :func:`@asgi() <.handlers.asgi>` route handler :term:`decorator` - from litestar.types import Scope, Receive, Send - from litestar.status_codes import HTTP_400_BAD_REQUEST - from litestar import Response, asgi + from litestar.types import Scope, Receive, Send + from litestar.status_codes import HTTP_400_BAD_REQUEST + from litestar import Response, asgi - @asgi(path="/my-asgi-app") - async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: + @asgi(path="/my-asgi-app") + async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": if scope["method"] == "GET": response = Response({"hello": "world"}) @@ -326,20 +332,20 @@ If you need to write your own ASGI application, you can do so using the :func:`a ) await response(scope=scope, receive=receive, send=send) -Like other route handlers, the ``asgi`` decorator is an alias of the class -:class:`ASGIRouteHandler <.handlers.ASGIRouteHandler>`. Thus, -the code below is equivalent to the one above: +Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator` is an alias of the +:class:`~.handlers.ASGIRouteHandler` class. Thus, the code below is equivalent to the one above: .. code-block:: python + :caption: Using the :class:`~.handlers.ASGIRouteHandler` class directly - from litestar import Response - from litestar.handlers.asgi_handlers import ASGIRouteHandler - from litestar.status_codes import HTTP_400_BAD_REQUEST - from litestar.types import Scope, Receive, Send + from litestar import Response + from litestar.handlers.asgi_handlers import ASGIRouteHandler + from litestar.status_codes import HTTP_400_BAD_REQUEST + from litestar.types import Scope, Receive, Send - @ASGIRouteHandler(path="/my-asgi-app") - async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: + @ASGIRouteHandler(path="/my-asgi-app") + async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": if scope["method"] == "GET": response = Response({"hello": "world"}) @@ -353,99 +359,108 @@ the code below is equivalent to the one above: Limitations of ASGI route handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In difference to the other route handlers, the ``asgi`` route handler accepts only 3 kwargs that **must** be defined: +In difference to the other route handlers, the :func:`@asgi() <.handlers.asgi>` route handler accepts only three +:term:`kwargs ` that **must** be defined: -* ``scope`` , a mapping of values describing the ASGI connection. It always includes a ``type`` key, with the values being - either ``http`` or ``websocket`` , and a ``path`` key. If the type is ``http`` , the scope dictionary will also include - a ``method`` key with the value being one of ``DELETE, GET, POST, PATCH, PUT, HEAD``. -* ``receive`` , an injected function by which the ASGI application receives messages. -* ``send`` , an injected function by which the ASGI application sends messages. +* ``scope``, a mapping of values describing the ASGI connection. It always includes a ``type`` key, with the values being + either ``http`` or ``websocket``, and a ``path`` key. If the type is ``http``, the scope dictionary will also include + a ``method`` key with the value being one of ``DELETE``, ``GET``, ``POST``, ``PATCH``, ``PUT``, ``HEAD``. +* ``receive``, an injected function by which the ASGI application receives messages. +* ``send``, an injected function by which the ASGI application sends messages. You can read more about these in the `ASGI specification `_. -Additionally, ASGI route handler functions **must** be async functions. This is enforced using inspection, and if the -function is not an async function, an informative exception will be raised. +Additionally, ASGI route handler functions **must** be :ref:`async functions `. +This is enforced using inspection, and if the function is not an :ref:`async functions `, +an informative exception will be raised. -See the :class:`API Reference <.handlers.asgi_handlers.ASGIRouteHandler>` for full details on the ``asgi`` decorator and the -kwargs it accepts. +See the :class:`ASGIRouteHandler API reference documentation <.handlers.asgi_handlers.ASGIRouteHandler>` for full +details on the :func:`@asgi() <.handlers.asgi>` :term:`decorator` and the :term:`kwargs ` it accepts. Route handler indexing ---------------------- -You can provide in all route handler decorators a ``name`` kwarg. The value for this kwarg **must be unique**\ , otherwise -:class:`ImproperlyConfiguredException ` exception will be raised. Default -value for ``name`` is value returned by ``handler.__str__`` which should be the full dotted path to the handler -(e.g. ``app.controllers.projects.list`` for ``list`` function residing in ``app/controllers/projects.py`` file). ``name`` can -be used to dynamically retrieve (i.e. during runtime) a mapping containing the route handler instance and paths, also -it can be used to build a URL path for that handler: +You can provide a :paramref:`~.handlers.base.BaseRouteHandler.name` :term:`kwarg ` in all route handler +:term:`decorators `. The value for this :term:`kwarg ` **must be unique**, otherwise +:exc:`~.exceptions.ImproperlyConfiguredException` exception will be raised. + +The default value for :paramref:`~.handlers.base.BaseRouteHandler.name` is value returned by the handler's +:meth:`~object.__str__` method which should be the full dotted path to the handler +(e.g., ``app.controllers.projects.list`` for ``list`` function residing in ``app/controllers/projects.py`` file). +:paramref:`~.handlers.base.BaseRouteHandler.name` can be used to dynamically retrieve (i.e. during runtime) a mapping +containing the route handler instance and paths, also it can be used to build a URL path for that handler: .. code-block:: python + :caption: Using the :paramref:`~.handlers.base.BaseRouteHandler.name` :term:`kwarg ` to retrieve a route + handler instance and paths + + from litestar import Litestar, Request, get + from litestar.exceptions import NotFoundException + from litestar.response import Redirect - from litestar import Litestar, Request, get - from litestar.exceptions import NotFoundException - from litestar.response import Redirect + @get("/abc", name="one") + def handler_one() -> None: + pass - @get("/abc", name="one") - def handler_one() -> None: - pass + @get("/xyz", name="two") + def handler_two() -> None: + pass - @get("/xyz", name="two") - def handler_two() -> None: - pass + @get("/def/{param:int}", name="three") + def handler_three(param: int) -> None: + pass - @get("/def/{param:int}", name="three") - def handler_three(param: int) -> None: - pass + @get("/{handler_name:str}", name="four") + def handler_four(request: Request, name: str) -> Redirect: + handler_index = request.app.get_handler_index_by_name(name) + if not handler_index: + raise NotFoundException(f"no handler matching the name {name} was found") - @get("/{handler_name:str}", name="four") - def handler_four(request: Request, name: str) -> Redirect: - handler_index = request.app.get_handler_index_by_name(name) - if not handler_index: - raise NotFoundException(f"no handler matching the name {name} was found") + # handler_index == { "paths": ["/"], "handler": ..., "qualname": ... } + # do something with the handler index below, e.g. send a redirect response to the handler, or access + # handler.opt and some values stored there etc. - # handler_index == { "paths": ["/"], "handler": ..., "qualname": ... } - # do something with the handler index below, e.g. send a redirect response to the handler, or access - # handler.opt and some values stored there etc. + return Redirect(path=handler_index[0]) - return Redirect(path=handler_index[0]) + @get("/redirect/{param_value:int}", name="five") + def handler_five(request: Request, param_value: int) -> Redirect: + path = request.app.route_reverse("three", param=param_value) + return Redirect(path=path) - @get("/redirect/{param_value:int}", name="five") - def handler_five(request: Request, param_value: int) -> Redirect: - path = request.app.route_reverse("three", param=param_value) - return Redirect(path=path) + app = Litestar(route_handlers=[handler_one, handler_two, handler_three]) - app = Litestar(route_handlers=[handler_one, handler_two, handler_three]) +:meth:`~.app.Litestar.route_reverse` will raise :exc:`~.exceptions.NoRouteMatchFoundException` if route with given +name was not found or if any of path :term:`parameters ` is missing or if any of passed path +:term:`parameters ` types do not match types in the respective route declaration. -:meth:`route_reverse <.app.Litestar.route_reverse>` will raise -:class:`NoMatchRouteFoundException <.exceptions.NoRouteMatchFoundException>` if route with given name was not found -or if any of path parameters is missing or if any of passed path parameters types do not match types in the respective -route declaration. However, :class:`str` is accepted in place of :class:`datetime.datetime`, :class:`datetime.date`, -:class:`datetime.time`, :class:`datetime.timedelta`, :class:`float`, and :class:`pathlib.Path` -parameters, so you can apply custom formatting and pass the result to ``route_reverse``. +However, :class:`str` is accepted in place of :class:`~datetime.datetime`, :class:`~datetime.date`, +:class:`~datetime.time`, :class:`~datetime.timedelta`, :class:`float`, and :class:`~pathlib.Path` +parameters, so you can apply custom formatting and pass the result to :meth:`~.app.Litestar.route_reverse`. -If handler has multiple paths attached to it ``route_reverse`` will return the path that consumes the most number of -keywords arguments passed to the function. +If handler has multiple paths attached to it :meth:`~.app.Litestar.route_reverse` will return the path that consumes +the most number of :term:`keyword arguments ` passed to the function. .. code-block:: python + :caption: Using the :meth:`~.app.Litestar.route_reverse` method to build a URL path for a route handler - from litestar import get, Request + from litestar import get, Request - @get( + @get( ["/some-path", "/some-path/{id:int}", "/some-path/{id:int}/{val:str}"], name="handler_name", - ) - def handler(id: int = 1, val: str = "default") -> None: ... + ) + def handler(id: int = 1, val: str = "default") -> None: ... - @get("/path-info") - def path_info(request: Request) -> str: + @get("/path-info") + def path_info(request: Request) -> str: path_optional = request.app.route_reverse("handler_name") # /some-path` @@ -457,60 +472,62 @@ keywords arguments passed to the function. return f"{path_optional} {path_partial} {path_full}" -If there are multiple paths attached to a handler that have the same path parameters (for example indexed handler -has been registered on multiple routers) the result of ``route_reverse`` is not defined. -The function will return a formatted path, but it might be picked randomly so reversing urls in such cases is highly -discouraged. - -If you have access to :class:`request <.connection.Request>` instance you can make reverse lookups using -:meth:`url_for <.connection.ASGIConnection.url_for>` function which is similar to ``route_reverse`` but returns -absolute URL. +When a handler is associated with multiple routes having identical path :term:`parameters ` +(e.g., an indexed handler registered across multiple routers), the output of :meth:`~.app.Litestar.route_reverse` is +unpredictable. This :term:`callable` will return a formatted path; however, its selection may appear arbitrary. +Therefore, reversing URLs under these conditions is **strongly** advised against. +If you have access to :class:`~.connection.Request` instance you can make reverse lookups using +:meth:`~.connection.ASGIConnection.url_for` method which is similar to :meth:`~.app.Litestar.route_reverse` but +returns an absolute URL. .. _handler_opts: Adding arbitrary metadata to handlers -------------------------------------- -All route handler decorators accept a key called ``opt`` which accepts a dictionary of arbitrary values, e.g. +All route handler :term:`decorators ` accept a key called ``opt`` which accepts a :term:`dictionary ` +of arbitrary values, e.g., .. code-block:: python + :caption: Adding arbitrary metadata to a route handler through the ``opt`` :term:`kwarg ` - from litestar import get + from litestar import get - @get("/", opt={"my_key": "some-value"}) - def handler() -> None: ... + @get("/", opt={"my_key": "some-value"}) + def handler() -> None: ... -This dictionary can be accessed by a :doc:`route guard `, or by accessing the ``route_handler`` -property on a :class:`request `, or using the -:class:`ASGI scope ` object directly. +This dictionary can be accessed by a :doc:`route guard `, or by accessing the +:attr:`~.connection.ASGIConnection.route_handler` property on a :class:`~.connection.request.Request` object, +or using the :class:`ASGI scope ` object directly. -Building on ``opts`` , you can pass any arbitrary kwarg to the route handler decorator, and it will be automatically set -as a key in the opt dictionary: +Building on ``opt``, you can pass any arbitrary :term:`kwarg ` to the route handler :term:`decorator`, +and it will be automatically set as a key in the ``opt`` dictionary: .. code-block:: python + :caption: Adding arbitrary metadata to a route handler through the ``opt`` :term:`kwarg ` - from litestar import get - + from litestar import get - @get("/", my_key="some-value") - def handler() -> None: ... + @get("/", my_key="some-value") + def handler() -> None: ... - assert handler.opt["my_key"] == "some-value" -You can specify the ``opt`` dictionary at all levels of your application. On specific route handlers, on a controller, -a router, and even on the app instance itself. + assert handler.opt["my_key"] == "some-value" -The resulting dictionary is constructed by merging opt dictionaries of all levels. If multiple layers define the same -key, the value from the closest layer to the response handler will take precedence. +You can specify the ``opt`` :term:`dictionary ` at all layers of your application. +On specific route handlers, on a controller, a router, and even on the app instance itself as described in +:ref:`layered architecture ` +The resulting :term:`dictionary ` is constructed by merging ``opt`` dictionaries of all layers. +If multiple layers define the same key, the value from the closest layer to the response handler will take precedence. .. _signature_namespace: -Signature namespace -------------------- +Signature :term:`namespace` +--------------------------- Litestar produces a model of the arguments to any handler or dependency function, called a "signature model" which is used for parsing and validation of raw data to be injected into the function. @@ -522,6 +539,7 @@ or ``flake8-type-checking`` will actively monitor, and suggest against. For example, the name ``Model`` is *not* available at runtime in the following snippet: .. code-block:: python + :caption: A route handler with a type that is not available at runtime from __future__ import annotations @@ -538,12 +556,12 @@ For example, the name ``Model`` is *not* available at runtime in the following s def create_item(data: Model) -> Model: return data - In this example, Litestar will be unable to generate the signature model because the type ``Model`` does not exist in the module scope at runtime. We can address this on a case-by-case basis by silencing our linters, for example: .. code-block:: python :no-upgrade: + :caption: Silencing linters for a type that is not available at runtime from __future__ import annotations @@ -554,39 +572,35 @@ the module scope at runtime. We can address this on a case-by-case basis by sile # Choose the appropriate noqa directive according to your linter from domain import Model # noqa: TCH002 -However, this approach can get tedious, so as an alternative, Litestar accepts a ``signature_types`` sequence at -every :ref:`layer ` of the application. The following is a demonstration of how to use this -pattern. - -This module defines our domain type in some central place. +However, this approach can get tedious; as an alternative, Litestar accepts a ``signature_types`` sequence at +every :ref:`layer ` of the application, as demonstrated in the following example: .. literalinclude:: /examples/signature_namespace/domain.py - :language: python + :caption: This module defines our domain type in some central place. -This module defines our controller, note that we don't import ``Model`` into the runtime namespace, nor do we require -any directives to control behavior of linters. +This module defines our controller, note that we do not import ``Model`` into the runtime :term:`namespace`, +nor do we require any directives to control behavior of linters. .. literalinclude:: /examples/signature_namespace/controller.py - :language: python + :caption: This module defines our controller without importing ``Model`` into the runtime namespace. Finally, we ensure that our application knows that when it encounters the name "Model" when parsing signatures, that it should reference our domain ``Model`` type. .. literalinclude:: /examples/signature_namespace/app.py - :language: python - -.. tip:: + :caption: Ensuring the application knows how to resolve the ``Model`` type when parsing signatures. - If you want to map your type to a name that is different from its ``__name__`` attribute, you can use the - ``signature_namespace`` parameter, e.g. ``app = Litestar(signature_namespace={"FooModel": Model})``. +.. tip:: If you want to map your type to a name that is different from its ``__name__`` attribute, + you can use the :paramref:`~.handlers.base.BaseRouteHandler.signature_namespace` parameter, + e.g., ``app = Litestar(signature_namespace={"FooModel": Model})``. This enables import patterns like ``from domain.foo import Model as FooModel`` inside ``if TYPE_CHECKING`` blocks. -Default signature namespace -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Default signature :term:`namespace` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Litestar automatically adds some names to the signature namespace when parsing signature models in order to support -injection of the :ref:`handler-function-kwargs`. +Litestar automatically adds some names to the signature :term:`namespace` when parsing signature models in +order to support injection of the :ref:`handler-function-kwargs`. These names are: diff --git a/docs/usage/routing/index.rst b/docs/usage/routing/index.rst index cca0bec64e..5d3d27bd1c 100644 --- a/docs/usage/routing/index.rst +++ b/docs/usage/routing/index.rst @@ -1,8 +1,15 @@ +======= Routing ======= +Routing is a fundamental aspect of building web applications with Litestar. +It involves mapping URLs to the appropriate request handlers that process and respond to client requests. + +Litestar provides a flexible and intuitive routing system that allows you to define routes at various +layers of your application. .. toctree:: + :caption: Articles overview handlers diff --git a/docs/usage/routing/overview.rst b/docs/usage/routing/overview.rst index a300e1a920..a2494b6aea 100644 --- a/docs/usage/routing/overview.rst +++ b/docs/usage/routing/overview.rst @@ -1,180 +1,185 @@ -Routing -======= - +======== +Overview +======== Registering Routes ------------------- At the root of every Litestar application there is an instance of the :class:`Litestar ` class, -on which the root level controllers, routers, and route handler functions are registered using the ``route_handlers`` kwarg: +on which the root level :class:`controllers <.controller.Controller>`, :class:`routers <.router.Router>`, +and :class:`route handler <.handlers.BaseRouteHandler>` functions are registered using the +:paramref:`~litestar.config.app.AppConfig.route_handlers` :term:`kwarg `: .. code-block:: python + :caption: Registering route handlers - from litestar import Litestar, get + from litestar import Litestar, get - @get("/sub-path") - def sub_path_handler() -> None: ... + @get("/sub-path") + def sub_path_handler() -> None: ... - @get() - def root_handler() -> None: ... + @get() + def root_handler() -> None: ... - app = Litestar(route_handlers=[root_handler, sub_path_handler]) + app = Litestar(route_handlers=[root_handler, sub_path_handler]) -Components registered on the app are appended to the root path. Thus, the ``root_handler`` function will be called for the -path ``"/"``, whereas the ``sub_path_handler`` will be called for ``"/sub-path"``. You can also declare a function to handle -multiple paths, e.g.: +Components registered on the app are appended to the root path. Thus, the ``root_handler`` function will be called for +the path ``"/"``, whereas the ``sub_path_handler`` will be called for ``"/sub-path"``. -.. code-block:: python +You can also declare a function to handle multiple paths, e.g.: - from litestar import get, Litestar +.. code-block:: python + :caption: Registering a route handler for multiple paths + from litestar import get, Litestar - @get(["/", "/sub-path"]) - def handler() -> None: ... + @get(["/", "/sub-path"]) + def handler() -> None: ... - app = Litestar(route_handlers=[handler]) -To handle more complex path schemas you should use routers and controllers + app = Litestar(route_handlers=[handler]) +To handle more complex path schemas you should use :class:`controllers <.controller.Controller>` and +:class:`routers <.router.Router>` Registering routes dynamically ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Occasionally there is a need for dynamic route registration. Litestar supports this via the ``.register`` method exposed -by the Litestar app instance: +Occasionally there is a need for dynamic route registration. Litestar supports this via the +:paramref:`~.app.Litestar.register` method exposed by the Litestar app instance: .. code-block:: python + :caption: Registering a route handler dynamically with the :paramref:`~.app.Litestar.register` method - from litestar import Litestar, get + from litestar import Litestar, get - @get() - def root_handler() -> None: ... + @get() + def root_handler() -> None: ... - app = Litestar(route_handlers=[root_handler]) + app = Litestar(route_handlers=[root_handler]) - @get("/sub-path") - def sub_path_handler() -> None: ... + @get("/sub-path") + def sub_path_handler() -> None: ... - app.register(sub_path_handler) + app.register(sub_path_handler) -Since the app instance is attached to all instances of :class:`ASGIConnection <.connection.base.ASGIConnection>`, -:class:`Request <.connection.request.Request>`, and :class:`WebSocket <.connection.websocket.WebSocket>` objects, you can in -effect call the :meth:`register <.router.Router.register>` method inside route handler functions, middlewares, and even +Since the app instance is attached to all instances of :class:`~.connection.base.ASGIConnection`, +:class:`~.connection.request.Request`, and :class:`~.connection.websocket.WebSocket` objects, you can in +effect call the :meth:`~.router.Router.register` method inside route handler functions, middlewares, and even injected dependencies. For example: .. code-block:: python + :caption: Call the :meth:`~.router.Router.register` method from inside a route handler function - from typing import Any - from litestar import Litestar, Request, get + from typing import Any + from litestar import Litestar, Request, get - @get("/some-path") - def route_handler(request: Request[Any, Any]) -> None: + @get("/some-path") + def route_handler(request: Request[Any, Any]) -> None: @get("/sub-path") def sub_path_handler() -> None: ... request.app.register(sub_path_handler) - app = Litestar(route_handlers=[route_handler]) - -In the above we dynamically created the sub-path_handler and registered it inside the ``route_handler`` function. + app = Litestar(route_handlers=[route_handler]) -.. caution:: +In the above we dynamically created the ``sub_path_handler`` and registered it inside the ``route_handler`` function. - Although Litestar exposes the :meth:`register <.router.Router.register>` method, it should not be abused. Dynamic - route registration increases the application complexity and makes it harder to reason about the code. It should - therefore be used only when absolutely required. +.. caution:: Although Litestar exposes the :meth:`register <.router.Router.register>` method, it should not be abused. + Dynamic route registration increases the application complexity and makes it harder to reason about the code. + It should therefore be used only when absolutely required. +:class:`Routers <.router.Router>` +--------------------------------- -Routers -------- +:class:`Routers <.router.Router>` are instances of the :class:`~.router.Router`, +class which is the base class for the :class:`Litestar app <.app.Litestar>` itself. -Routers are instances of :class:`litestar.router.Router <.router.Router>`, which is the base class for the -:class:`Litestar app <.app.Litestar>` itself. A router can register Controllers, route handler functions, and other routers, -similarly to the Litestar constructor: +A :class:`~.router.Router` can register :class:`Controllers <.controller.Controller>`, +:class:`route handler <.handlers.BaseRouteHandler>` functions, and other routers, similarly to the Litestar constructor: .. code-block:: python + :caption: Registering a :class:`~.router.Router` - from litestar import Litestar, Router, get + from litestar import Litestar, Router, get - @get("/{order_id:int}") - def order_handler(order_id: int) -> None: ... + @get("/{order_id:int}") + def order_handler(order_id: int) -> None: ... - order_router = Router(path="/orders", route_handlers=[order_handler]) - base_router = Router(path="/base", route_handlers=[order_router]) - app = Litestar(route_handlers=[base_router]) + order_router = Router(path="/orders", route_handlers=[order_handler]) + base_router = Router(path="/base", route_handlers=[order_router]) + app = Litestar(route_handlers=[base_router]) -Once ``order_router`` is registered on ``base_router``\ , the handler function registered on ``order_router`` will +Once ``order_router`` is registered on ``base_router``, the handler function registered on ``order_router`` will become available on ``/base/orders/{order_id}``. +:class:`Controllers <.controller.Controller>` +--------------------------------------------- +:class:`Controllers <.controller.Controller>` are subclasses of the :class:`Controller <.controller.Controller>` class. +They are used to organize endpoints under a specific sub-path, which is the controller's path. +Their purpose is to allow users to utilize Python OOP for better code organization and organize code by logical concerns. -Controllers ------------ - -Controllers are subclasses of the class :class:`Controller <.controller.Controller>`. They are used to organize endpoints -under a specific sub-path, which is the controller's path. Their purpose is to allow users to utilize python OOP for -better code organization and organize code by logical concerns. +.. dropdown:: Click to see an example of registering a controller -.. code-block:: python + .. code-block:: python + :caption: Registering a :class:`~.controller.Controller` - from litestar.contrib.pydantic import PydanticDTO - from litestar.controller import Controller - from litestar.dto import DTOConfig, DTOData - from litestar.handlers import get, post, patch, delete - from pydantic import BaseModel, UUID4 + from litestar.contrib.pydantic import PydanticDTO + from litestar.controller import Controller + from litestar.dto import DTOConfig, DTOData + from litestar.handlers import get, post, patch, delete + from pydantic import BaseModel, UUID4 - class UserOrder(BaseModel): - user_id: int - order: str + class UserOrder(BaseModel): + user_id: int + order: str - class PartialUserOrderDTO(PydanticDTO[UserOrder]): - config = DTOConfig(partial=True) + class PartialUserOrderDTO(PydanticDTO[UserOrder]): + config = DTOConfig(partial=True) - class UserOrderController(Controller): - path = "/user-order" + class UserOrderController(Controller): + path = "/user-order" - @post() - async def create_user_order(self, data: UserOrder) -> UserOrder: ... + @post() + async def create_user_order(self, data: UserOrder) -> UserOrder: ... - @get(path="/{order_id:uuid}") - async def retrieve_user_order(self, order_id: UUID4) -> UserOrder: ... + @get(path="/{order_id:uuid}") + async def retrieve_user_order(self, order_id: UUID4) -> UserOrder: ... - @patch(path="/{order_id:uuid}", dto=PartialUserOrderDTO) - async def update_user_order( - self, order_id: UUID4, data: DTOData[PartialUserOrderDTO] - ) -> UserOrder: ... + @patch(path="/{order_id:uuid}", dto=PartialUserOrderDTO) + async def update_user_order( + self, order_id: UUID4, data: DTOData[PartialUserOrderDTO] + ) -> UserOrder: ... - @delete(path="/{order_id:uuid}") - async def delete_user_order(self, order_id: UUID4) -> None: ... + @delete(path="/{order_id:uuid}") + async def delete_user_order(self, order_id: UUID4) -> None: ... The above is a simple example of a "CRUD" controller for a model called ``UserOrder``. You can place as many :doc:`route handler methods ` on a controller, as long as the combination of path+http method is unique. -The ``path`` that is defined on the Controller is appended before the path that is defined for the route handlers declared -on it. Thus, in the above example, ``create_user_order`` has the path of the controller - ``/user-order/`` , -while ``retrieve_user_order`` has the path ``/user-order/{order_id:uuid}"``. - -.. note:: - - If you do not declare a ``path`` class variable on the controller, it will default to the root path of ``"/"``. - +The ``path`` that is defined on the :class:`controller <.controller.Controller>` is appended before the path that is +defined for the route handlers declared on it. Thus, in the above example, ``create_user_order`` has the path of the +:class:`controller <.controller.Controller>` - ``/user-order/``, while ``retrieve_user_order`` has the path +``/user-order/{order_id:uuid}"``. +.. note:: If you do not declare a ``path`` class variable on the controller, it will default to the root path of ``"/"``. Registering components multiple times -------------------------------------- @@ -185,27 +190,29 @@ Controllers ^^^^^^^^^^^ .. code-block:: python + :caption: Registering a controller multiple times - from litestar import Router, Controller, get + from litestar import Router, Controller, get - class MyController(Controller): + class MyController(Controller): path = "/controller" @get() def handler(self) -> None: ... - internal_router = Router(path="/internal", route_handlers=[MyController]) - partner_router = Router(path="/partner", route_handlers=[MyController]) - consumer_router = Router(path="/consumer", route_handlers=[MyController]) + internal_router = Router(path="/internal", route_handlers=[MyController]) + partner_router = Router(path="/partner", route_handlers=[MyController]) + consumer_router = Router(path="/consumer", route_handlers=[MyController]) In the above, the same ``MyController`` class has been registered on three different routers. This is possible because -what is passed to the router is not a class instance but rather the class itself. The router creates its own instance of -the controller, which ensures encapsulation. +what is passed to the :class:`router <.router.Router>` is not a class instance but rather the class itself. +The :class:`router <.router.Router>` creates its own instance of the :class:`controller <.controller.Controller>`, +which ensures encapsulation. Therefore, in the above example, three different instances of ``MyController`` will be created, each mounted on a -different sub-path, e.g. ``/internal/controller``\ , ``/partner/controller``, and ``/consumer/controller``. +different sub-path, e.g., ``/internal/controller``, ``/partner/controller``, and ``/consumer/controller``. Route handlers ^^^^^^^^^^^^^^ @@ -213,43 +220,40 @@ Route handlers You can also register standalone route handlers multiple times: .. code-block:: python + :caption: Registering a route handler multiple times - from litestar import Litestar, Router, get + from litestar import Litestar, Router, get - @get(path="/handler") - def my_route_handler() -> None: ... + @get(path="/handler") + def my_route_handler() -> None: ... - internal_router = Router(path="/internal", route_handlers=[my_route_handler]) - partner_router = Router(path="/partner", route_handlers=[my_route_handler]) - consumer_router = Router(path="/consumer", route_handlers=[my_route_handler]) + internal_router = Router(path="/internal", route_handlers=[my_route_handler]) + partner_router = Router(path="/partner", route_handlers=[my_route_handler]) + consumer_router = Router(path="/consumer", route_handlers=[my_route_handler]) - Litestar(route_handlers=[internal_router, partner_router, consumer_router]) + Litestar(route_handlers=[internal_router, partner_router, consumer_router]) When the handler function is registered, it's actually copied. Thus, each router has its own unique instance of the route handler. Path behaviour is identical to that of controllers above, namely, the route handler -function will be accessible in the following paths: ``/internal/handler`` , ``/partner/handler``, and ``/consumer/handler``. - -.. attention:: +function will be accessible in the following paths: ``/internal/handler``, ``/partner/handler``, and ``/consumer/handler``. - You can nest routers as you see fit - but be aware that once a router has been registered it cannot be +.. attention:: You can nest routers as you see fit - but be aware that once a router has been registered it cannot be re-registered or an exception will be raised. - - Mounting ASGI Apps ------------------- -Litestar support "mounting" ASGI applications on sub paths, that is - specifying a handler function that will handle all +Litestar support "mounting" ASGI applications on sub-paths, i.e., specifying a handler function that will handle all requests addressed to a given path. -.. literalinclude:: /examples/routing/mount_custom_app.py - :caption: Mounting an ASGI App - :language: python +.. dropdown:: Click to see an example of mounting an ASGI app + .. literalinclude:: /examples/routing/mount_custom_app.py + :caption: Mounting an ASGI App -The handler function will receive all requests with an url that begins with ``/some/sub-path`` , e.g. ``/some/sub-path``, +The handler function will receive all requests with an url that begins with ``/some/sub-path``, e.g, ``/some/sub-path``, ``/some/sub-path/abc``, ``/some/sub-path/123/another/sub-path``, etc. .. admonition:: Technical Details @@ -259,13 +263,14 @@ The handler function will receive all requests with an url that begins with ``/s the value of ``scope["path"]`` will equal ``"/`"``. If we send a request to ``/some/sub-path/abc``, it will also be invoked,and ``scope["path"]`` will equal ``"/abc"``. -Mounting is especially useful when you need to combine components of other ASGI applications - for example, for 3rd party libraries. -The following example is identical in principle to the one above, but it uses `Starlette `_: +Mounting is especially useful when you need to combine components of other ASGI applications - for example, for third +party libraries. The following example is identical in principle to the one above, but it uses +`Starlette `_: -.. literalinclude:: /examples/routing/mounting_starlette_app.py - :caption: Mounting a Starlette App - :language: python +.. dropdown:: Click to see an example of mounting a Starlette app + .. literalinclude:: /examples/routing/mounting_starlette_app.py + :caption: Mounting a Starlette App .. admonition:: Why Litestar uses radix based routing @@ -280,8 +285,6 @@ The following example is identical in principle to the one above, but it uses `S Litestar implements its routing solution that is based on the concept of a `radix tree `_ or *trie*. - .. seealso:: - - If you are interested in the technical aspects of the implementation, refer to + .. seealso:: If you are interested in the technical aspects of the implementation, refer to `this GitHub issue `_ - it includes an indepth discussion of the pertinent code. diff --git a/docs/usage/routing/parameters.rst b/docs/usage/routing/parameters.rst index 2abbb4b44c..e362eb801c 100644 --- a/docs/usage/routing/parameters.rst +++ b/docs/usage/routing/parameters.rst @@ -4,220 +4,208 @@ Parameters Path Parameters --------------- -Path parameters are parameters declared as part of the ``path`` component of the URL. They are declared using a simple -syntax ``{param_name:param_type}`` : +Path :term:`parameters ` are parameters declared as part of the ``path`` component of +the URL. They are declared using a simple syntax ``{param_name:param_type}`` : .. literalinclude:: /examples/parameters/path_parameters_1.py - :language: python - - + :caption: Defining a path parameter in a route handler In the above there are two components: -1. The path parameter is defined in the ``@get`` decorator, which declares both the parameter's name (``user_id``) and type (``int``). -2. The decorated function ``get_user`` defines a parameter with the same name as the parameter defined in the ``path`` kwarg. +1. The path :term:`parameter` is defined in the :class:`@get() <.handlers.get>` :term:`decorator`, which declares both + the parameter's name (``user_id``) and type (:class:`int`). +2. The :term:`decorated ` function ``get_user`` defines a parameter with the same name as the + parameter defined in the ``path`` :term:`kwarg `. -The correlation of parameter name ensures that the value of the path parameter will be injected into the function when -it's called. +The correlation of parameter name ensures that the value of the path parameter will be injected into +the function when it is called. Supported Path Parameter Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Currently, the following types are supported: - * ``date``: Accepts date strings and time stamps. * ``datetime``: Accepts date-time strings and time stamps. * ``decimal``: Accepts decimal values and floats. -* ``float``: Accepts ints and floats. -* ``int``: Accepts ints and floats. -* ``path``: Accepts valid POSIX paths. -* ``str``: Accepts all string values. +* :class:`float`: Accepts ints and floats. +* :class:`int`: Accepts ints and floats. +* :class:`path`: Accepts valid POSIX paths. +* :class:`str`: Accepts all string values. * ``time``: Accepts time strings with optional timezone compatible with pydantic formats. * ``timedelta``: Accepts duration strings compatible with the pydantic formats. * ``uuid``: Accepts all uuid values. -The types declared in the path parameter and the function do not need to match 1:1 - as long as parameter inside the -function declaration is typed with a "higher" type to which the lower type can be coerced, this is fine. For example, -consider this: +The types declared in the path :term:`parameter` and the function do not need to match 1:1 - as long as +parameter inside the function declaration is typed with a "higher" type to which the lower type can be coerced, +this is fine. For example, consider this: .. literalinclude:: /examples/parameters/path_parameters_2.py - :language: python + :caption: Coercing path parameters into different types +The :term:`parameter` defined inside the ``path`` :term:`kwarg ` is typed as :class:`int` , because the value +passed as part of the request will be a timestamp in milliseconds without any decimals. The parameter in +the function declaration though is typed as :class:`datetime.datetime`. +This works because the int value will be passed to a pydantic model representing the function signature, which will +coerce the :class:`int` into a :class:`~datetime.datetime`. -The parameter defined inside the ``path`` kwarg is typed as :class:`int` , because the value passed as part of the request will be -a timestamp in milliseconds without any decimals. The parameter in the function declaration though is typed -as :class:`datetime.datetime`. This works because the int value will be passed to a pydantic model representing the function -signature, which will coerce the int into a datetime. Thus, when the function is called it will be called with a -datetime typed parameter. +Thus, when the function is called it will be called with a :class:`~datetime.datetime`-typed parameter. -.. note:: +.. note:: You only need to define the :term:`parameter` in the function declaration if it is actually used inside the + function. If the path parameter is part of the path, but the function does not use it, it is fine to omit + it. It will still be validated and added to the OpenAPI schema correctly. - You only need to define the parameter in the function declaration if it's actually used inside the function. If the - path parameter is part of the path, but the function doesn't use it, it's fine to omit it. It will still be validated - and added to the openapi schema correctly. +The Parameter function +---------------------- +:func:`~.params.Parameter` is a helper function wrapping a :term:`parameter` with extra information to be +added to the OpenAPI schema. Extra validation and documentation for path params ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to add validation or enhance the OpenAPI documentation generated for a given path parameter, you can do -so using the `the parameter function`_: +If you want to add validation or enhance the OpenAPI documentation generated for a given path :term:`parameter`, +you can do so using the `the parameter function`_: .. literalinclude:: /examples/parameters/path_parameters_3.py - :language: python - - -In the above example, :func:`Parameter <.params.Parameter>` is used to restrict the value of ``version`` to a range -between 1 and 10, and then set the ``title``, ``description``, ``examples``, and ``externalDocs`` sections of the -OpenAPI schema. + :caption: Adding extra validation and documentation to a path parameter +In the above example, :func:`~.params.Parameter` is used to restrict the value of :paramref:`~.params.Parameter.version` +to a range between 1 and 10, and then set the :paramref:`~.params.Parameter.title`, +:paramref:`~.params.Parameter.description`, :paramref:`~.params.Parameter.examples`, and +:paramref:`externalDocs <.params.Parameter.external_docs>` sections of the OpenAPI schema. Query Parameters ---------------- -Query parameters are defined as keyword arguments to handler functions. Every keyword argument -that is not otherwise specified (for example as a :ref:`path parameter `) -will be interpreted as a query parameter. +Query :term:`parameters ` are defined as :term:`keyword arguments ` to handler functions. +Every :term:`keyword argument ` that is not otherwise specified (for example as a +:ref:`path parameter `) will be interpreted as a query parameter. .. literalinclude:: /examples/parameters/query_params.py - :language: python - + :caption: Defining query parameters in a route handler .. admonition:: Technical details :class: info - These parameters will be parsed from the function signature and used to generate a pydantic model. + These :term:`parameters ` will be parsed from the function signature and used to generate a Pydantic model. This model in turn will be used to validate the parameters and generate the OpenAPI schema. This means that you can also use any pydantic type in the signature, and it will follow the same kind of validation and parsing as you would get from pydantic. -Query parameters come in three basic types: - +Query :term:`parameters ` come in three basic types: - Required - Required with a default value - Optional with a default value Query parameters are **required** by default. If one such a parameter has no value, -a :class:`ValidationException <.exceptions.http_exceptions.ValidationException>` will be raised. - +a :exc:`~.exceptions.http_exceptions.ValidationException` will be raised. Default values ~~~~~~~~~~~~~~ -In this example, ``param`` will have the value ``"hello"`` if it's not specified in the request. -If it's passed as a query parameter however, it will be overwritten: +In this example, ``param`` will have the value ``"hello"`` if it is not specified in the request. +If it is passed as a query :term:`parameter` however, it will be overwritten: .. literalinclude:: /examples/parameters/query_params_default.py - :language: python - + :caption: Defining a default value for a query parameter -Optional parameters -~~~~~~~~~~~~~~~~~~~ +Optional :term:`parameters ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Instead of only setting a default value, it's also possible to make a query parameter -entirely optional. +Instead of only setting a default value, it is also possible to make a query parameter entirely optional. Here, we give a default value of ``None`` , but still declare the type of the query parameter -to be a string. This means that this parameter is not required. If it is given, it has to be a string. +to be a :class:`string `. This means that this parameter is not required. + +If it is given, it has to be a :class:`string `. If it is not given, it will have a default value of ``None`` .. literalinclude:: /examples/parameters/query_params_optional.py - :language: python - + :caption: Defining an optional query parameter Type coercion ------------- -It is possible to coerce query parameters into different types. A query starts out as a string, -but its values can be parsed into all kinds of types. Since this is done by pydantic, -everything that works there will work for query parameters as well. - -.. literalinclude:: /examples/parameters/query_params_types.py - :language: python +It is possible to coerce query :term:`parameters ` into different types. +A query starts out as a :class:`string `, but its values can be parsed into all kinds of types. +Since this is done by Pydantic, everything that works there will work for query parameters as well. +.. literalinclude:: /examples/parameters/query_params_types.py + :caption: Coercing query parameters into different types Alternative names and constraints --------------------------------- -Sometimes you might want to "remap" query parameters to allow a different name in the URL -than what's being used in the handler function. This can be done by making use of -:func:`Parameter <.params.Parameter>`. +Sometimes you might want to "remap" query :term:`parameters ` to allow a different name in the URL +than what is being used in the handler function. This can be done by making use of :func:`~.params.Parameter`. .. literalinclude:: /examples/parameters/query_params_remap.py - :language: python - + :caption: Remapping query parameters to different names Here, we remap from ``snake_case`` in the handler function to ``camelCase`` in the URL. This means that for the URL ``http://127.0.0.1:8000?camelCase=foo`` , the value of ``camelCase`` will be used for the value of the ``snake_case`` parameter. -``Parameter`` also allows us to define additional constraints: +:func:`~.params.Parameter` also allows us to define additional constraints: .. literalinclude:: /examples/parameters/query_params_constraints.py - :language: python - + :caption: Constraints on query parameters In this case, ``param`` is validated to be an *integer larger than 5*. - Header and Cookie Parameters ---------------------------- -Unlike *Query* parameters, *Header* and *Cookie* parameters have to be declared using -`the parameter function`_ , for example: +Unlike *Query* :term:`parameters `, *Header* and *Cookie* parameters have to be +declared using `the parameter function`_ , for example: .. literalinclude:: /examples/parameters/header_and_cookie_parameters.py - :language: python - - - -As you can see in the above, header parameters are declared using the ``header`` kwargs and cookie parameters using -the ``cookie`` kwarg. Aside form this difference they work the same as query parameters. - - -The Parameter function ------------------------ - -:func:`Parameter <.params.Parameter>` is a helper function wrapping a parameter with extra information to be added to -the OpenAPI schema. + :caption: Defining header and cookie parameters +As you can see in the above, header parameters are declared using the ``header`` +:term:`kwargs ` and cookie parameters using the ``cookie`` :term:`kwarg `. +Aside form this difference they work the same as query parameters. Layered Parameters -------------------- +------------------ -As part of Litestar's layered architecture, you can declare parameters not only as part of individual route handler -functions, but also on other layers of the application: +As part of Litestar's :ref:`layered architecture `, you can declare +:term:`parameters ` not only as part of individual route handler functions, but also on other layers +of the application: .. literalinclude:: /examples/parameters/layered_parameters.py - :language: python + :caption: Declaring parameters on different layers of the application +In the above we declare :term:`parameters ` on the :class:`Litestar app <.app.Litestar>`, +:class:`router <.router.Router>`, and :class:`controller <.controller.Controller>` layers in addition to those +declared in the route handler. Now, examine these more closely. +* ``app_param`` is a cookie parameter with the key ``special-cookie``. We type it as :class:`str` by passing + this as an arg to the :func:`~.params.Parameter` function. This is required for us to get typing in the OpenAPI doc. + Additionally, this parameter is assumed to be required because it is not explicitly set as ``False`` on + :paramref:`~.params.Parameter.required`. -In the above we declare parameters on the app, router and controller levels in addition to those declared in the route -handler. Let's look at these closer. + This is important because the route handler function does not declare a parameter called ``app_param`` at all, + but it will still require this param to be sent as part of the request of validation will fail. +* ``router_param`` is a header parameter with the key ``MyHeader``. Because it is set as ``False`` on + :paramref:`~.params.Parameter.required`, it will not fail validation if not present unless explicitly declared by a + route handler - and in this case it is. -* ``app_param`` is a cookie param with the key ``special-cookie``. We type it as ``str`` by passing this as an arg to - the ``Parameter`` function. This is required for us to get typing in the OpenAPI docs. Additionally, this parameter is - assumed to be required because it is not explicitly declared as ``required=False``. This is important because the route - handler function does not declare a parameter called ``app_param`` at all, but it will still require this param to be - sent as part of the request of validation will fail. -* ``router_param`` is a header param with the key ``MyHeader``. Because its declared as ``required=False`` , it will not fail - validation if not present unless explicitly declared by a route handler - and in this case it is. Thus, it is actually - required for the router handler function that declares it as an ``str`` and not an ``Optional[str]``. If a string value is - provided, it will be tested against the provided regex. -* ``controller_param`` is a query param with the key ``controller_param``. It has an ``lt=100`` defined on the controller, - which - means the provided value must be less than 100. Yet the route handler re-declares it with an ``lt=50`` , which means for - the route handler this value must be less than 50. -* ``local_param`` is a route handler local query parameter, and ``path_param`` is a path parameter. + Thus, it is actually required for the router handler function that declares it as an :class:`str` and not an + ``str | None``. If a :class:`string ` value is provided, it will be tested against the provided regex. +* ``controller_param`` is a query param with the key ``controller_param``. It has an :paramref:`~.params.Parameter.lt` + set to ``100`` defined on the controller, which means the provided value must be less than 100. -.. note:: + Yet the route handler re-declares it with an :paramref:`~.params.Parameter.lt` set to ``50``, + which means for the route handler this value must be less than 50. +* ``local_param`` is a route handler local :ref:`query parameter `, and + ``path_param`` is a :ref:`path parameter `. - You cannot declare path parameters in different application layers. The reason for this is to ensure - simplicity - otherwise parameter resolution becomes very difficult to do correctly. +.. note:: You cannot declare path :term:`parameters ` in different application layers. The reason for this + is to ensure simplicity - otherwise parameter resolution becomes very difficult to do correctly. diff --git a/pyproject.toml b/pyproject.toml index 33d0b2bb3b..34df602bb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,15 +131,16 @@ dev = [ ] dev-contrib = ["opentelemetry-sdk", "httpx-sse"] docs = [ - "sphinx>=7.1.2", - "sphinx-autobuild>=2021.3.14", - "sphinx-copybutton>=0.5.2", - "sphinx-toolbox>=3.5.0", - "sphinx-design>=0.5.0", - "sphinx-click>=4.4.0", - "sphinxcontrib-mermaid>=0.9.2", - "auto-pytabs[sphinx]>=0.4.0", - "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git", + "sphinx>=7.1.2", + "sphinx-autobuild>=2021.3.14", + "sphinx-copybutton>=0.5.2", + "sphinx-toolbox>=3.5.0", + "sphinx-design>=0.5.0", + "sphinx-click>=4.4.0", + "sphinxcontrib-mermaid>=0.9.2", + "auto-pytabs[sphinx]>=0.4.0", + "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git", + "sphinx-paramlinks>=0.6.0", ] linting = [ "ruff>=0.2.1",