litestar路由

0 阅读14分钟

litestar路由

本文内容基于《官方文档-路由》的部分进行制作,对其内容大幅进行缩减,使其能适合新手入门,如何想充分学习litestar可以直接查看官方文档

Litestar的路由与FastAPI类似

1. 简单使用

应用直接注册路由 在应用上注册的组件会附加到根路径

from litestar import Litestar, get

@get()
def root_handler() -> None: ...

#@get(path="/sub-path") 可以是指定参数
@get("/sub-path")
def sub_path_handler() -> None: ...

@get(["/", "/sub-path1"])
def handler() -> None: ...

# path参数获取
@get(["/some-path", "/some-path/{some_id:int}"])
async def my_route_handler(some_id: int = 1) -> None: ...

app = Litestar(route_handlers=[root_handler, sub_path_handler, handler])

其他动作:

  • @delete()
  • @get()
  • @head()
  • @patch()
  • @post()
  • @put()

同时支持多种类型:

不建议使用,相反,更推荐的模式是通过辅助类方法或把代码抽象成可复用函数来实现代码共享。

from litestar import HttpMethod, route


@route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
async def my_endpoint() -> None: ..

# 等价于:
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: ...

2. 同步和异步可调用对象

Litestar默认采用异步方式进行编写,其实其他方式不是设置将会警告

同步和异步可调用对象均受支持。其中一个重要方面是,使用执行阻塞操作(如 I/O 或计算密集型任务)的同步函数,有可能阻塞运行事件循环的主线程,进而阻塞整个应用程序。 为缓解此问题,可将 sync_to_thread 参数设置为 True,这将使函数在线程池中运行。 如果同步函数是非阻塞的,将 sync_to_thread 设置为 False 会告知 Litestar,用户确定该函数的行为,且该函数可被视为非阻塞函数。 如果传递了一个同步函数,但未设置明确的 sync_to_thread 值,将会发出警告。

2.1. “保留” 关键字参数

路由处理函数或方法通过将各种数据声明为带注释的函数关键字参数来进行访问。Litestar 会检查这些带注释的关键字参数,然后将其注入到请求处理程序中。 可以使用带注释的函数关键字参数访问以下数据源:

  • path,query,header, 和cookie parameters
  • requests
  • injected dependencies

此外,你可以指定以下特殊的关键字参数(称为 “保留关键字”):

  • cookies:将请求中的 cookies 作为已解析的字典注入
  • headers:将请求头作为已解析的字典注入
  • query:将请求的查询参数(query_params)作为已解析的字典注入
  • request:注入 Request 实例。仅对 HTTP 路由处理程序可用
  • scope:注入 ASGI 作用域字典。
  • socket:注入 WebSocket 实例。仅对 WebSocket 路由处理程序可用
  • state:注入应用程序状态(State)的副本
  • body:原始请求体。仅对 HTTP 路由处理程序可用

请注意,如果你的参数与上述任何保留关键字参数冲突,你可以提供一个替代名称

例如:

from typing import Any
from litestar import Request, get
from litestar.datastructures import State


@get(path="/")
async def my_request_handler(
   state: State,
   request: Request,
   headers: dict[str, str],
   query: dict[str, Any],
   cookies: dict[str, Any],
) -> None: ...

提示:你可以为应用程序状态定义自定义类型,然后使用该自定义类型,而非仅使用 Litestar 中的State类

2.2. 类型注释

Litestar 强制使用严格的类型注解。由路由处理程序装饰的函数,其所有参数和返回值都必须进行类型注解。 如果缺少类型注解,在应用程序启动过程中将会抛出ImproperlyConfiguredException异常。

实施此限制有几个原因:

  • 确保采用最佳实践
  • 确保一致地生成 OpenAPI 模式
  • 使 Litestar 能够在应用程序引导期间计算函数所需的参数

3. WebSocket 路由处理程序

WebSocket 连接可以由@websocket()路由处理程序来处理。

注意:WebSocket 处理程序是一种底层方法,需要直接处理套接字,并处理保持其打开状态、异常、客户端断开连接以及内容协商等问题。若要以更高级的方式处理 WebSocket,请参阅 “WebSocket” 部分。

from litestar import WebSocket, websocket


@websocket(path="/socket")
async def my_websocket_handler(socket: WebSocket) -> None:
   await socket.accept()
   await socket.send_json({...})
   await socket.close()

@websocket()装饰器是WebsocketRouteHandler类的别名。因此,以下代码与上述代码等效:

from litestar import WebSocket
from litestar.handlers.websocket_handlers import WebsocketRouteHandler


@WebsocketRouteHandler(path="/socket")
async def my_websocket_handler(socket: WebSocket) -> None:
   await socket.accept()
   await socket.send_json({...})
   await socket.close()

与 HTTP 路由处理程序不同,WebSocket 处理程序有以下要求:

  • 它们必须声明一个名为socket的关键字参数。
  • 它们的返回值注解必须为None
  • 它们必须是异步函数。

这些要求通过检查来强制执行,如果其中任何一项未满足,将会抛出一条信息丰富的异常。 目前 OpenAPI 不支持 WebSocket。因此,不会为这些路由处理程序生成任何模式。

4. 参数

4.1. 路径参数

from pydantic import BaseModel
from litestar import Litestar, get

USER_DB = {1: {"id": 1, "name": "John Doe"}}

class User(BaseModel):
    id: int
    name: str

@get("/user/{user_id:int}", sync_to_thread=False)
def get_user(user_id: int) -> User:
    return User.model_validate(USER_DB[user_id])

app = Litestar(route_handlers=[get_user])

4.2. 支持的路径参数类型

类型格式与python格式一样

  • date
  • datetime
  • decimal
  • float
  • path
  • str
  • time
  • timedelta
  • uuid

在path参数和函数中声明的类型不需要1:1匹配 - 只要函数声明中的parameter是用"higher"类型键入的,而较低的类型可以被强制转换为,这很好。例如,请考虑

4.3. Parameter 函数

Parameter()是一个辅助函数,它使用要添加到 OpenAPI 架构的额外信息包装参数

4.3.1. 路径参数的额外验证和文档

如果要添加验证或增强为给定路径参数生成的 OpenAPI 文档,可以使用parameter函数来实现:

from pydantic import BaseModel, Json, conint
from typing import Annotated

from litestar import Litestar, get
from litestar.openapi.spec.example import Example
from litestar.openapi.spec.external_documentation import ExternalDocumentation
from litestar.params import Parameter

class Version(BaseModel):
    id: conint(ge=1, le=10)  # type: ignore[valid-type]
    specs: Json

VERSIONS = {1: Version(id=1, specs='{"some": "value"}')}

@get(path="/versions/{version:int}", sync_to_thread=False)
def get_product_version(
    version: Annotated[
        int,
        Parameter(
            ge=1,
            le=10,
            title="Available Product Versions",
            description="Get a specific version spec from the available specs",
            examples=[Example(value=1)],
            external_docs=ExternalDocumentation(
                url="https://mywebsite.com/documentation/product#versions",  # type: ignore[arg-type]
            ),
        ),
    ],
) -> Version:
    return VERSIONS[version]

app = Litestar(route_handlers=[get_product_version])

在上面的例子中,Parameter() 用于限制version的值 设置为 1 到 10 之间的范围,然后将标题描述、 示例和externalDocs部分。

4.4. 查询参数

查询参数定义为处理程序函数的关键字参数。每个未另行指定的关键字参数(例如,作为path 参数将被解释为查询参数。

from litestar import Litestar, get


@get("/", sync_to_thread=False)
def index(param: str) -> dict[str, str]:
    return {"param": param}


app = Litestar(route_handlers=[index])

'''
> curl http://127.0.0.1:8000/?param=foo
{"param":"foo"}
> curl http://127.0.0.1:8000/?param=bar
{"param":"bar"}
'''

这些参数将从函数签名中解析并用于生成内部数据模型。此模型反过来将用于验证参数并生成 OpenAPI 架构。 此功能允许您使用任意数量的模式/建模库,包括PydanticMsgspecAttrsDataclasses,并且它将遵循与从这些库中获得的相同类型的验证和解析。

4.4.1. 查询参数有三种基本类型
  • 必填,使用默认值
  • 可选,使用默认值

默认情况下,查询参数是必填的 。如果一个这样的参数没有值,将引发ValidationException

4.4.2. 默认值

在此示例中,如果未在请求中指定 param 的值 "hello" ,则该值将"hello"。但是,如果它作为查询参数传递,它将被覆盖:

from litestar import Litestar, get

@get("/", sync_to_thread=False)
def index(param: str = "hello") -> dict[str, str]:
    return {"param": param}

app = Litestar(route_handlers=[index])
'''
> curl http://127.0.0.1:8000/
{"param":"hello"}
> curl http://127.0.0.1:8000/?param=john
{"param":"john"}
'''
4.4.3. 可选参数

除了仅设置默认值外,还可以将查询参数设置为完全可选。 在这里,我们给出默认值 None ,但仍将查询参数的类型声明为字符串。这意味着此参数不是必需的。 如果给定,则它必须是一个字符串 。如果未给出,则其默认值为 None

from litestar import Litestar, get


@get("/", sync_to_thread=False)
def index(param: str | None = None) -> dict[str, str | None]:
    return {"param": param}


app = Litestar(route_handlers=[index])
'''
> curl http://127.0.0.1:8000/
{"param":null}
> curl http://127.0.0.1:8000/?param=goodbye
{"param":"goodbye"}
'''
4.4.4. 强制类型

可以将查询参数强制转换为不同的类型。查询开始时是一个字符串,但其值可以解析为各种类型。

from datetime import datetime, timedelta
from typing import Any

from litestar import Litestar, get


@get("/", sync_to_thread=False)
def index(date: datetime, number: int, floating_number: float, strings: list[str]) -> dict[str, Any]:
    return {
        "datetime": date + timedelta(days=1),
        "int": number,
        "float": floating_number,
        "list": strings,
    }


app = Litestar(route_handlers=[index])
'''
> curl http://127.0.0.1:8000/?date=2022-11-28T13:22:06.916540&floating_number=0.1&number=42&strings=1&strings=2
{"datetime":"2022-11-29T13:22:06.916540","int":42,"float":0.1,"list":["1","2"]}
'''
4.4.5. 参数别名和约束

有时,您可能希望 “重新映射” 查询参数,以允许 URL 中的名称与处理程序函数中使用的名称不同。这可以通过使用 Parameter()来完成。

from typing import Annotated

from litestar import Litestar, get
from litestar.params import Parameter


@get("/", sync_to_thread=False)
def index(snake_case: Annotated[str, Parameter(query="camelCase")]) -> dict[str, str]:
    return {"param": snake_case}


app = Litestar(route_handlers=[index])
'''
> curl http://127.0.0.1:8000/?camelCase=foo
{"param":"foo"}
'''

在这里,我们从处理程序函数中的 snake_case 重新映射到 URL 中的 camelCase。这意味着对于 URL http://127.0.0.1:8000?camelCase=foocamelCase 的值 将用于 snake_case 参数的值。

Parameter()还允许我们定义其他约束:

from typing import Annotated

from litestar import Litestar, get
from litestar.params import Parameter


@get("/", sync_to_thread=False)
def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]:
    return {"param": param}


app = Litestar(route_handlers=[index])

在这种情况下,param 被验证为大于 5 的整数

4.4.6. 标头和 Cookie 参数

Query参数不同,HeaderCookie参数必须使用参数 function 声明,例如:

from pydantic import BaseModel
from typing import Annotated

from litestar import Litestar, get
from litestar.exceptions import NotAuthorizedException
from litestar.params import Parameter

USER_DB = {
    1: {
        "id": 1,
        "name": "John Doe",
    },
}

VALID_TOKEN = "super-secret-secret"
VALID_COOKIE_VALUE = "cookie-secret"


class User(BaseModel):
    id: int
    name: str


@get(path="/users/{user_id:int}/")
async def get_user(
    user_id: int,
    token: Annotated[str, Parameter(header="X-API-KEY")],
    cookie: Annotated[str, Parameter(cookie="my-cookie-param")],
) -> User:
    if token != VALID_TOKEN or cookie != VALID_COOKIE_VALUE:
        raise NotAuthorizedException
    return User.model_validate(USER_DB[user_id])


app = Litestar(route_handlers=[get_user])

如上所示,标头参数是使用header声明的,而 cookie 参数是使用 cookie声明的。除了这种差异之外,它们的工作方式与 query parameters 相同。

4.4.7. 分层参数

作为Litestar分层架构的一部分,您可以声明parameter不仅作为单个路由处理程序函数的一部分,而且在app的其他层上:

from typing import Annotated

from litestar import Controller, Litestar, Router, get
from litestar.params import Parameter


class MyController(Controller):
    path = "/controller"
    parameters = {
        "controller_param": Parameter(int, lt=100),
    }

    @get("/{path_param:int}", sync_to_thread=False)
    def my_handler(
        self,
        path_param: int,
        local_param: str,
        router_param: str,
        controller_param: Annotated[int, Parameter(int, lt=50)],
    ) -> dict[str, str | int]:
        return {
            "path_param": path_param,
            "local_param": local_param,
            "router_param": router_param,
            "controller_param": controller_param,
        }


router = Router(
    path="/router",
    route_handlers=[MyController],
    parameters={
        "router_param": Parameter(str, pattern="^[a-zA-Z]$", header="MyHeader", required=False),
    },
)

app = Litestar(
    route_handlers=[router],
    parameters={
        "app_param": Parameter(str, cookie="special-cookie"),
    },
)

不能在不同的应用程序层中声明路径参数 。这样做的原因是为了确保简单性 - 否则参数解析将变得非常难以正确进行。

5. 注册路由

Litestar 通过register方法进行路由注册:

from litestar import Litestar, get

@get()
def root_handler() -> None: ...

app = Litestar(route_handlers=[root_handler])

@get("/sub-path")
def sub_path_handler() -> None: ...

app.register(sub_path_handler)

由于应用实例会附加到ASGIConnectionRequestWebSocket对象的所有实例上,因此实际上你可以在路由处理函数、中间件甚至注入的依赖项中调用register () 方法。例如:

from typing import Any
from litestar import Litestar, Request, get

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

在上述内容中,我们动态创建了sub_path_handler并在路由处理函数内部对其进行了注册

警告:尽管Litestar暴露了register方法,但不应滥用该方法。动态路由注册会增加应用程序的复杂性,使其更难理解代码逻辑。因此,只有在绝对必要时才应使用此功能。

5.1. Routers

RoutersRouter类的实例,而Router类是Litestar应用程序本身的基类。 与Litestar构造函数类似,路由管理器可以注册控制器、路由处理函数以及其他路由管理器,示例如下:

from litestar import Litestar, Router, get


@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()base_router 上完成注册,那么在 order_router 上注册的处理函数就可以通过 /base/orders/{order_id} 路径来访问

5.2. Controllers

ControllersController 类的子类。它们用于在特定的子路径(即控制器的路径)下组织端点。其目的是让用户能够利用 Python 的面向对象编程特性,实现更好的代码组织,并依据逻辑关注点来编排代码

案例:

from litestar.plugins.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 PartialUserOrderDTO(PydanticDTO[UserOrder]):
   config = DTOConfig(partial=True)


class UserOrderController(Controller):
   path = "/user-order"

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

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

上述内容是一个针对名为 UserOrder 的模型的 “增删改查(CRUD)” 控制器的简单示例。只要路径与 HTTP 方法的组合是唯一的,你就可以在一个控制器 上设置任意数量的路由处理方法。

在控制器上定义的路径会被添加到在该控制器上声明的路由处理程序所定义的路径之前。因此,在上述示例中,create_user_order 的路径是控制器的路径 /user-order/,而 retrieve_user_order 的路径则是 /user-order/{order_id:uuid}

注意:如果未在控制器上声明 path 类变量,其默认路径为根路径"/"

6. 多次注册

你可以多次注册独立的路由处理函数和控制器。

6.1. Controllers

from litestar import Router, Controller, get


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

在上述示例中,同一个 MyController 类已在三个不同的路由管理器上进行了注册。这是可行的,因为传递给路由管理器的并非类的实例,而是类本身。路由管理器会创建属于自己的控制器实例,以此确保封装性。

因此,在上述示例中,将会创建三个不同的 MyController 实例,每个实例都挂载在不同的子路径上,例如 /internal/controller/partner/controller/consumer/controller

6.2. Route handlers

你还可以多次注册独立的路由处理函数

from litestar import Litestar, Router, get


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

Litestar(route_handlers=[internal_router, partner_router, consumer_router])

当注册处理函数时,实际上会对其进行复制操作。因此,每个路由管理器都拥有该路由处理函数的一个独立实例。路径的处理规则与上述控制器的情况相同,也就是说,该路由处理函数可以通过以下路径进行访问:/internal/handler/partner/handler/consumer/handler

注意:你可以根据需要嵌套路由器,但要注意,一旦注册了某个路由器,就不能再次注册,否则会引发异常

7. 路径索引(动态路径解析)

可以通过name='<唯一名称>'的方式

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("/xyz", name="two")
def handler_two() -> 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")

    # 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])


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

如果未找到将会引起NoRouteMatchFoundException报错

如果您有权访问 Request 实例,则可以使用url_for()方法,该方法类似于 route_reverse(), 但返回绝对 URL