Ir para o conteúdo

Routing

O Lilya possui um sistema de roteamento simples, mas altamente eficaz, capaz de lidar desde rotas simples até as mais complexas.

Usando uma aplicação empresarial como exemplo, o sistema de roteamento certamente não será algo simples com 20 ou 40 rotas diretas, talvez tenha 200 ou 300 rotas onde essas são divididas por responsabilidades, componentes e pacotes e também importadas dentro de sistemas de design complexos. O Lilya lida com esses casos sem nenhum tipo de problema.

Router

O Router é o objecto principal que liga todo o Lilya ao Path, WebSocketPath e Include.

Classe Router

A classe do router é composta por muitos atributos que são preenchidos por defeito dentro da aplicação, mas o Lilya também permite adicionar routers personalizados adicionais, bem como adicionar uma aplicação ChildLilya.

from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path


async def create(request: Request):
    await request.json()
    ...


app = Lilya(
    routes=[
        Path(
            "/create",
            handler=create,
            methods=["POST"],
        )
    ],
)

A classe principal Router é instanciada dentro da aplicação Lilya com as rotas fornecidas e a aplicação é iniciada.

Parâmetros

Ao criar um handler Path ou WebSocketPath, tem duas maneiras de obter os parâmetros do caminho.

  • Lilya descobre e injeta automaticamente.
  • Obtém-nos directamente do objecto request.

Descobrindo automaticamente os parâmetros

Esta é provavelmente a maneira mais fácil e simples.

from lilya.apps import Lilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Include, Path


def update(item_id: int):
    return item_id


def another_update(request: Request):
    item_id = request.path_params["item_id"]
    return JSONResponse({"Success", {item_id}})


app = Lilya(
    routes=[
        Include(
            "/update",
            routes=[
                Path(
                    "/update/partial/{item_id:int}",
                    handler=update,
                    methods=["PATCH"],
                ),
                Path(
                    path="/last/{item_id:int}",
                    handler=another_update,
                    methods=["PATCH"],
                ),
            ],
        )
    ]
)

O customer_id declarado no path também foi declarado no handler, permitindo que o Lilya injete os valores encontrados por ordem dos parâmetros do caminho.

A partir dos parâmetros do caminho do request.

from lilya.apps import Lilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path


def update(request: Request):
    item_id = request.path_params["item_id"]
    return JSONResponse({"Success", {item_id}})


app = Lilya(
    routes=[
        Path(
            "/update/{item_id:int}",
            handler=update,
            methods=["PUT"],
        ),
    ]
)

O customer_id declarado no path foi obtido acedendo ao objecto request.

Router Personalizado

Vamos supor que existam submódulos específicos de clientes dentro de um ficheiro dedicado customers. Existem duas maneiras de separar as rotas dentro da aplicação, utilizando Include, um ChildLilya ou criando outro router. Vamos focar neste último.

/application/apps/routers/customers.py
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path, Router


async def create(request: Request):
    await request.json()
    return JSONResponse({"created": True})


async def get_customer(customer_id: int):
    return JSONResponse({"created": customer_id})


router = Router(
    path="/customers",
    routes=[
        Path(
            "/{customer_id:int}",
            handler=get_customer,
        ),
        Path(
            "/create",
            handler=create,
            methods=["POST"],
        ),
    ],
)

Acima, cria o /application/apps/routers/customers.py com todas as informações necessárias. Não precisa estar num único ficheiro, pode ter um pacote completamente separado apenas para gerir o cliente.

Agora precisa adicionar o novo router personalizado à aplicação principal.

/application/app.py
from lilya.apps import Lilya
from lilya.routing import Include

app = Lilya(
    routes=[
        Include(
            "/",
            app=...,
        )
    ]
)

Isto é simples e o router é adicionado à aplicação principal do Lilya.

Aplicação ChildLilya

O que é isto? Chamamos de ChildLilya, mas na verdade é apenas o Lilya, mas com um nome diferente, principalmente para fins de visualização e organização.

Como funciona

Vamos usar o mesmo exemplo utilizado nos routers personalizados com as rotas e regras específicas dos clientes.

/application/apps/routers/customers.py
from lilya.apps import ChildLilya
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path


async def create(request: Request) -> JSONResponse:
    await request.json()
    return JSONResponse({"created": True})


async def get_customer() -> JSONResponse:
    return JSONResponse({"created": True})


router = ChildLilya(
    routes=[
        Path("/{customer_id:int}", handler=get_customer),
        Path("/create", handler=create, methods=["POST"]),
    ],
    include_in_schema=...,
)

Como o ChildLilya é uma representação de uma classe Lilya, podemos passar os parâmetros limitados do router personalizado e todos os parâmetros disponíveis no Lilya.

Pode adicionar quantos ChildLilya desejar, não há limites.

Agora na aplicação principal:

/application/app.py
from apps.routers.customers import router as customers_router

from lilya.apps import Lilya
from lilya.routing import Include

app = Lilya(
    routes=[
        Include("/customers", app=customers_router),
    ]
)

Adicionando aplicações nested

/application/app.py
from apps.routers.clients import router as clients_router
from apps.routers.customers import router as customers_router
from apps.routers.restrict import router as restrict_router

from lilya.apps import Lilya
from lilya.routing import Include

app = Lilya(
    routes=[
        Include("/customers", app=customers_router),
        Include(
            "/api/v1",
            routes=[
                Include("/clients", clients_router),
                Include("/restrict", routes=[Include("/access", restrict_router)]),
            ],
        ),
    ]
)

O exemplo acima mostra que pode até adicionar a mesma aplicação dentro de includes nested e para cada include, pode adicionar permissões e middlewares específicos, que estão disponíveis em cada instância do Include. As opções são infinitas.

Note

Em termos de organização, o ChildLilya tem uma abordagem limpa para isolamento de responsabilidades e permite tratar cada módulo individualmente e simplesmente adicioná-lo à aplicação principal sobe a forma de Include.

Tip

Trate o ChildLilya como uma instância independente do Lilya.

Check

Ao adicionar uma aplicação ChildLilya ou Lilya, não se esqueça de adicionar o caminho único para o Include base, dessa forma pode garantir que as rotas sejam corretamente encontradas.

Utilitários

O objecto Router possui algumas funcionalidades disponíveis que podem ser úteis.

add_route()

from lilya.apps import Lilya

app = Lilya()

app.add_route(
    path=...,
    methods=...,
    name=...,
    middleware=...,
    permissions=...,
    include_in_schema=...,
)

Parâmetros

  • path - O caminho para o ChildLilya.
  • name - Nome da rota.
  • handler - O handler.
  • methods - Os verbos HTTP disponíveis para o caminho.
  • include_in_schema - Se a rota deve ser adicionada ao OpenAPI Schema (se tiver algum).
  • permissions - Uma lista de permissões para atender aos pedidos de entrada da aplicação (HTTP e Websockets).
  • middleware - Uma lista de middlewares para executar em cada pedido.
  • exception handlers - Um dicionário de tipos de excepção (ou excepções personalizadas) e os handlers no nível superior da aplicação. Os exception handlers devem ter a forma handler(request, exc) -> response e podem ser funções padrão ou funções assíncronas.

add_websocket_route()

from lilya.apps import Lilya

app = Lilya()

app.add_websocket_route(
    path=...,
    handler=...,
    name=...,
    middleware=...,
    permissions=...,
)

Parâmetros

  • path - O caminho para o ChildLilya.
  • name - Nome da rota.
  • handler - O handler.
  • permissions - Uma lista de permissões para atender aos pedidos de entrada da aplicação (HTTP e Websockets).
  • middleware - Uma lista de middlewares para executar em cada pedido.
  • exception handlers - Um dicionário de tipos de exceção (ou exceções personalizadas) e os handlers no nível superior da aplicação. Os exception handlers devem ter a forma handler(request, exc) -> response e podem ser funções padrão ou funções assíncronas.

add_child_lilya()

from lilya.apps import ChildLilya, Lilya
from lilya.routing import Path


async def home() -> str:
    return "home"


child = ChildLilya(
    routes=[
        Path("/", handler=home, name="view"),
    ]
)

app = Lilya()

app.add_child_lilya(
    path="/child",
    child=child,
    name=...,
    middleware=...,
    permissions=...,
    include_in_schema=...,
    deprecated=...,
)

Parâmetros

  • path - O caminho para o ChildLilya.
  • child - A instância ChildLilya.
  • name - Nome da rota.
  • handler - O handler.
  • permissions - Uma lista de permissões para atender aos pedidos de entrada da aplicação (HTTP e Websockets).
  • middleware - Uma lista de middlewares para executar em cada pedido.
  • exception handlers - Um dicionário de tipos de exceção (ou exceções personalizadas) e os handlers no nível superior da aplicação. Os exception handlers devem ter a forma handler(request, exc) -> response e podem ser funções padrão ou funções assíncronas.
  • include_in_schema - Booleano se este ChildLilya deve ser incluído no esquema.
  • deprecated - Booleano se este ChildLilya deve ser marcado como obsoleto.

Path

O objecto que liga e constrói as URLs ou caminhos da aplicação. Ele mapeia o handler com o sistema de roteamento da aplicação.

Parâmetros

  • path - O caminho para o ChildLilya.
  • name - Nome da rota.
  • handler - O handler.
  • methods - Os verbos HTTP disponíveis para o caminho.
  • include_in_schema - Se a rota deve ser adicionada ao OpenAPI Schema.
  • permissions - Uma lista de permissões para atender aos pedidos de entrada da aplicação (HTTP e Websockets).
  • middleware - Uma lista de middlewares para executar em cada pedido.
  • exception handlers - Um dicionário de tipos de exceção (ou exceções personalizadas) e os handlers no nível superior da aplicação. Os exception handlers devem ter a forma handler(request, exc) -> response e podem ser funções padrão ou funções assíncronas.
  • deprecated - Booleano se este ChildLilya deve ser marcado como obsoleto.
from lilya.apps import Lilya, Request
from lilya.routing import Path


async def homepage(request: Request) -> str:
    return "Hello, home!"


app = Lilya(
    routes=[
        Path(
            handler=homepage,
        )
    ]
)

WebSocketPath

O mesmo princípio do Path com uma particularidade. Os websockets são async.

Parâmetros

  • path - O caminho para o ChildLilya.
  • name - Nome da rota.
  • handler - O handler.
  • include_in_schema - Se a rota deve ser adicionada ao OpenAPI Schema.
  • permissions - Uma lista de permissões para atender aos pedidos de entrada da aplicação (HTTP e Websockets).
  • middleware - Uma lista de middlewares para executar em cada pedido.
  • exception handlers - Um dicionário de tipos de exceção (ou exceções personalizadas) e os handlers no nível superior da aplicação. Os exception handlers devem ter a forma handler(request, exc) -> response e podem ser funções padrão ou funções assíncronas.
  • deprecated - Booleano se este ChildLilya deve ser marcado como obsoleto.
from lilya.apps import Lilya
from lilya.routing import WebSocketPath
from lilya.websockets import WebSocket


async def world_socket(websocket: WebSocket) -> None:
    await websocket.accept()
    msg = await websocket.receive_json()
    assert msg
    assert websocket
    await websocket.close()


app = Lilya(
    routes=[
        WebSocketPath(
            "/{path_param:str}",
            handler=world_socket,
        ),
    ]
)

Include

Os Includes são exclusivos do Lilya, poderosos e com mais controlo, permitindo:

  1. Escalabilidade sem problemas.
  2. Design de roteamento limpo.
  3. Separation of Concerns.
  4. Separação de rotas.
  5. Redução do nível de importações necessárias através de ficheiros.
  6. Menos erros humanos.

Warning

Os Includes NÃO aceitam parâmetros de caminho. Por exemplo: Include('/include/{id:int}, routes=[...]).

Include e aplicação

Este é um objecto muito especial que permite a importação de qualquer rota de qualquer lugar da aplicação. O Include aceita a importação via namespace ou via lista routes, mas não ambos.

Ao usar um namespace, o Include procurará a lista padrão route_patterns no namespace (objecto) importado, a menos que um pattern diferente seja especificado.

O padrão só funciona se as importações forem feitas via namespace e não via objecto routes.

Parâmetros

  • path - O caminho para o ChildLilya.
  • app - Uma aplicação pode ser qualquer coisa tratada como uma aplicação ASGI. O app pode ser uma aplicação relacionada com ASGI ou uma string <dotted>.<module> com a localização da aplicação.
  • routes - Uma lista global de rotas Lilya. Essas rotas podem variar e podem ser Path, WebSocketPath ou até mesmo outro Include.
  • namespace - Uma string com um namespace qualificado de onde as URLs devem ser carregadas.
  • pattern - Uma string com informações de pattern de onde as URLs devem ser lidas.
  • name - Nome do Include.
  • permissions - Uma lista de permissões para atender aos pedidos de entrada da aplicação (HTTP e Websockets).
  • middleware - Uma lista de middlewares para executar em cada pedido.
  • exception handlers - Um dicionário de tipos de exceção (ou exceções personalizadas) e os handlers no nível superior da aplicação. Os exception handlers devem ter a forma handler(request, exc) -> response e podem ser funções padrão ou funções assíncronas.
  • include_in_schema - Se a rota deve ser adicionada ao OpenAPI Schema.
  • deprecated - Booleano se este Include deve ser marcado como obsoleto.
myapp/urls.py
from lilya.routing import Include

route_patterns = [
    Include("/", namespace="myapp.accounts.urls"),
]
src/myapp/urls.py
from myapp.accounts.urls import route_patterns

from lilya.routing import Include

route_patterns = [
    Include("/", routes=route_patterns),
]

Esta é uma alternativa para carregar a aplicação via importação string em vez de passar o objecto diretamente.

src/myapp/urls.py
from lilya.routing import Include

# There is an app in the location `myapp.asgi_or_wsgi.apps.child_lilya`

route_patterns = [
    Include(
        "/child",
        app="myapp.asgi_or_wsgi.apps.child_lilya",
    ),
]

Usando um padrão diferente

src/myapp/accounts/controllers.py
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.websockets import WebSocket


async def update_product(request: Request, product_id: int):
    data = await request.json()
    return {"product_id": product_id, "product_name": data["name"]}


async def home():
    return JSONResponse({"detail": "Hello world"})


async def another(request: Request):
    return {"detail": "Another world!"}


async def world_socket(websocket: WebSocket):
    await websocket.accept()
    msg = await websocket.receive_json()
    assert msg
    assert websocket
    await websocket.close()
src/myapp/accounts/urls.py
from lilya.routing import Path, WebSocketPath

from .views import another, home, update_product, world_socket

my_urls = [
    Path(
        "/product/{product_id}",
        handler=update_product,
        methods=["PUT"],
    ),
    Path("/", handler=home),
    Path("/another", handler=another),
    WebSocketPath(
        "/{path_param:str}",
        handler=world_socket,
    ),
]
src/myapp/urls.py
from lilya.routing import Include

route_patterns = [
    Include(
        "/",
        namespace="myapp.accounts.urls",
        pattern="my_urls",
    ),
]

Include e instância da aplicação

O Include pode ser muito útil, principalmente quando o objetivo é evitar muitas importações e uma lista massiva de objectos a serem passados para um único objecto. Isso pode ser especialmente útil para criar um objecto Lilya limpo.

Exemplo:

src/urls.py
from lilya.routing import Include

route_patterns = [
    Include(
        "/",
        namespace="myapp.accounts.urls",
        pattern="my_urls",
    )
]
src/app.py
from lilya.apps import Lilya
from lilya.routing import Include

app = Lilya(
    routes=[
        Include("/", namespace="src.urls"),
    ]
)

Rotas nested

À medida que a complexidade aumenta e o nível de rotas também aumenta, o Include permite rotas nested de forma limpa.

from lilya.apps import Lilya
from lilya.routing import Include, Path


async def me() -> None: ...


app = Lilya(
    routes=[
        Include(
            "/",
            routes=[
                Path(
                    path="/me",
                    handler=me,
                )
            ],
        )
    ]
)
from lilya.apps import Lilya
from lilya.routing import Include, Path


async def me() -> None: ...


app = Lilya(
    routes=[
        Include(
            "/",
            routes=[
                Include(
                    "/another",
                    routes=[
                        Include(
                            "/multi",
                            routes=[
                                Include(
                                    "/nested",
                                    routes=[
                                        Include(
                                            "/routing",
                                            routes=[
                                                Path(path="/me", handler=me),
                                                Include(
                                                    path="/imported",
                                                    namespace="myapp.routes",
                                                ),
                                            ],
                                        )
                                    ],
                                )
                            ],
                        )
                    ],
                )
            ],
        )
    ]
)

Include suporta quantas rotas nested com caminhos e Includes diferentes desejar ter. Assim que a aplicação é iniciada, as rotas são montadas e imediatamente disponíveis.

Rotas nested também permitem todas as funcionalidades em cada nível, desde middlewares até permissões.

Rotas da aplicação

Warning

Tenha muito cuidado ao utilizar o Include diretamente no Lilya(routes[]), importar sem um path pode resultar nalgumas rotas não serem mapeadas corretamente.

Aplicável apenas às rotas da aplicação:

Se decidir fazer isso:

from lilya.apps import Lilya
from lilya.routing import Include

app = Lilya(
    routes=[
        Include(
            "/",
            namespace="src.urls",
            name="root",
        ),
        Include(
            "/accounts",
            namespace="accounts.v1.urls",
            name="accounts",
        ),
    ]
)

Host

Se deseja utilizar rotas distintas para o mesmo caminho, dependendo do cabeçalho Host, o Lilya fornece uma solução.

É importante observar que o porto é ignorado do cabeçalho Host durante a correspondência. Por exemplo, Host(host='example.com:8081', ...) será processado independentemente de o cabeçalho Host conter um porto diferente de 8081 (por exemplo, example.com:8083, example.org). Portanto, se o porto for essencial para fins de path_for, pode especificá-la explicitamente.

Existem várias abordagens para estabelecer rotas baseadas em host para o sua aplicação.

from lilya.apps import Lilya
from lilya.routing import Host, Router

internal = Router()
api = Router()
external = Router()

routes = [Host("api.example.com", api, name="api")]
app = Lilya(routes=routes)

app.host("www.example.com", internal, name="intenal_site")
external_host = Host("external.example.com", external)

app.router.routes.append(external)

As pesquisas de URL podem abranger parâmetros de host, assim como os parâmetros de caminho são incluídos.

from lilya.requests import Request
from lilya.routing import Host, Include, Path, Router


def user(): ...


def detail(username: str): ...


routes = [
    Host(
        "{subdomain}.example.com",
        name="sub",
        app=Router(
            routes=[
                Include(
                    "/users",
                    name="users",
                    routes=[
                        Path("/", user, name="user"),
                        Path("/{username}", detail, name="detail"),
                    ],
                )
            ]
        ),
    )
]

request = Request(...)

url = request.path_for("sub:users:user", username=..., subdomain=...)
url = request.path_for("sub:users:detail", subdomain=...)

Prioridade das Rotas

As rotas da aplicação são simplesmente prioritizadas. Os caminhos de entrada são comparados com cada Path, WebSocketPath e Include ordenados.

Nos casos em que mais de um Path pode corresponder a um caminho de entrada, deve garantir que as rotas mais específicas sejam listadas antes dos casos gerais.

Exemplo:

from lilya.apps import Lilya
from lilya.routing import Path


async def user(): ...


async def active_user(): ...


# Don't do this: `/users/me`` will never match the incoming requests.
app = Lilya(
    routes=[
        Path(
            "/users/{username}",
            handler=user,
        ),
        Path(
            "/users/me",
            handler=active_user,
        ),
    ]
)

# Do this: `/users/me` is tested first and both cases will work.
app = Lilya(
    routes=[
        Path(
            "/users/me",
            handler=active_user,
        ),
        Path(
            "/users/{username}",
            handler=user,
        ),
    ]
)

Parâmetros de Caminho

Os caminhos podem usar um estilo de template para os componentes do caminho. Os parâmetros de caminho são aplicados apenas a Path e WebSocketPath e não são aplicados ao Include.

Lembre-se de que existem duas maneiras de lidar com os parâmetros de caminho.

async def customer(customer_id: Union[int, str]) -> None:
    ...


async def floating_point(number: float) -> None:
    ...

Path("/customers/{customer_id}/example", handler=customer)
Path("/floating/{number:float}", handler=customer)

Por defeito, isto capturará caracteres até o final do caminho do próximo / e se ficará /customers/{customer_id}/example.

Transformadores podem ser usados para modificar o que está a ser capturado e o tipo do que está a ser capturado. O Lilya fornece alguns transformadores de caminho por defeito.

  • str retorna uma string e é o padrão.
  • int retorna um inteiro Python.
  • float retorna um float Python.
  • uuid retorna uma instância uuid.UUID Python.
  • path retorna o restante caminho, incluindo quaisquer caracteres / adicionais.
  • datetime retorna a data e hora.

Conforme o padrão, os transformadores são usados prefixando-os com dois pontos:

Path('/customers/{customer_id:int}', handler=customer)
Path('/floating-point/{number:float}', handler=floating_point)
Path('/uploaded/{rest_of_path:path}', handler=uploaded)

Transformadores Personalizados

Se houver a necessidade de um transformador diferente que não esteja definido ou disponível, também pode criar o seu.

from __future__ import annotations

import ipaddress

from lilya.transformers import Transformer, register_path_transformer


class IPTransformer(Transformer[str]):
    regex = r"((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$"

    def transform(self, value: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address:
        return ipaddress.ip_address(value)

    def normalise(self, value: ipaddress.IPv4Address | ipaddress.IPv6Address) -> str:
        return str(value)


register_path_transformer("ipaddress", IPTransformer())

Com o transformador personalizado criado, agora pode usá-lo.

Path('/network/{address:ipaddress}', handler=network)

Middlewares, Exception Handlers e Permissões

Exemplos

Os exemplos a seguir são aplicados à Path, WebSocketPath e Include.

Usaremos o Path, pois pode ser substituído por qualquer um dos acima, visto que é comum entre eles.

Middlewares

Conforme especificado anteriormente, os middlewares de um Path são lidos de cima para baixo, do pai até o próprio handler, e o mesmo é aplicado aos exception handlers e permissões.

from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.protocols.middleware import MiddlewareProtocol
from lilya.routing import Path
from lilya.types import ASGIApp


class RequestLoggingMiddlewareProtocol(MiddlewareProtocol):
    def __init__(self, app: "ASGIApp", kwargs: str = "") -> None:
        self.app = app
        self.kwargs = kwargs


class ExampleMiddleware(MiddlewareProtocol):
    def __init__(self, app: "ASGIApp") -> None:
        self.app = app


async def homepage():
    return {"page": "ok"}


app = Lilya(
    routes=[
        Path(
            "/home",
            handler=homepage,
            middleware=[DefineMiddleware(ExampleMiddleware)],
        )
    ],
    middleware=[
        DefineMiddleware(RequestLoggingMiddlewareProtocol),
    ],
)

O exemplo acima ilustra os vários níveis em que um middleware pode ser implementado e, porque segue uma ordem de pai, a ordem é:

  1. Middlewares internos padrão da aplicação.
  2. RequestLoggingMiddlewareProtocol.
  3. ExampleMiddleware.

Mais do que um middleware pode ser adicionado a cada lista.

Exception Handlers

from lilya.apps import Lilya
from lilya.exceptions import InternalServerError, LilyaException, NotAuthorized
from lilya.requests import Request
from lilya.responses import JSONResponse
from lilya.routing import Path


async def http_lilya_handler(_: Request, exc: LilyaException):
    return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)


async def http_internal_server_error_handler(_: Request, exc: InternalServerError):
    return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)


async def http_not_authorized_handler(_: Request, exc: NotAuthorized):
    return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)


async def homepage() -> dict:
    return {"page": "ok"}


app = Lilya(
    routes=[
        Path(
            "/home",
            handler=homepage,
            exception_handlers={
                NotAuthorized: http_not_authorized_handler,
                InternalServerError: http_internal_server_error_handler,
            },
        )
    ],
    exception_handlers={LilyaException: http_lilya_handler},
)

O exemplo acima ilustra os vários níveis em que os exception handlers podem ser implementados e segue uma ordem de pai, onde a ordem é:

  1. Exception handlers internos padrão da aplicação.
  2. InternalServerError: http_internal_server_error_handler.
  3. NotAuthorized: http_not_authorized_handler.

Mais de um handler pode ser adicionado a cada mapeamento.

Permissões

Permissões são obrigatórias em todas as aplicações. Saiba mais sobre permissões e como usá-las.

from lilya.apps import Lilya
from lilya.exceptions import PermissionDenied
from lilya.permissions import DefinePermission
from lilya.protocols.permissions import PermissionProtocol
from lilya.requests import Request
from lilya.routing import Include, Path
from lilya.types import ASGIApp, Receive, Scope, Send


class AllowAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-access" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


class AdminAccess(PermissionProtocol):
    def __init__(self, app: ASGIApp, *args, **kwargs):
        super().__init__(app, *args, **kwargs)
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope=scope, receive=receive, send=send)

        if "allow-admin" in request.headers:
            await self.app(scope, receive, send)
            return
        raise PermissionDenied()


async def home():
    return "Hello world"


async def user(user: str):
    return f"Hello {user}"


# Via Path
app = Lilya(
    routes=[
        Path("/", handler=home),
        Path(
            "/{user}",
            handler=user,
            permissions=[
                DefinePermission(AdminAccess),
            ],
        ),
    ],
    permissions=[DefinePermission(AllowAccess)],
)


# Via Include
app = Lilya(
    routes=[
        Include(
            "/",
            routes=[
                Path("/", handler=home),
                Path(
                    "/{user}",
                    handler=user,
                    permissions=[
                        DefinePermission(AdminAccess),
                    ],
                ),
            ],
            permissions=[DefinePermission(AllowAccess)],
        )
    ]
)

Mais de uma permissão pode ser adicionada a cada lista.

Pesquisa Reversa de Caminho

Frequentemente, há a necessidade de gerar o URL para uma rota específica, especialmente em cenários em que uma resposta de reencaminhamento é necessária.

from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Path


def user(): ...


app = Lilya(
    routes=[
        Path("/user", user, name="user"),
    ]
)

request = Request(...)

# Path lookup here
path = request.path_for("user")

As pesquisas também permitem parâmetros de caminho.

from lilya.apps import Lilya
from lilya.requests import Request
from lilya.routing import Include, Path


def detail(): ...


app = Lilya(
    routes=[
        Include(
            "/users",
            routes=[
                Path("/{username}", detail, name="detail"),
            ],
            name="users",
        )
    ]
)

request = Request(...)

# Path lookup here
path = request.path_for("users:detail", username=...)

Se um Include incluir um nome, as submontagens subsequentes devem usar o formato {prefix}:{name} para pesquisas de caminho reverso.

Usando o reverse

Esta é uma alternativa para a pesquisa reversa de caminho. Pode ser particularmente útil se você quiser reverter um caminho em testes ou isoladamente.

Parâmetros

  • name - O nome atribuído ao caminho.
  • app - Uma aplicação ASGI que contém as rotas. Útil para reverter caminhos em aplicações específicas e/ou testes. (Opcional).
  • path_params - Um objecto semelhante a um dicionário contendo os parâmetros que devem ser passados num determinado caminho. (Opcional).

Ao usar o reverse, se nenhum parâmetro app for especificado, será automaticamente definido como a aplicação ou roteador de aplicação, que em circunstâncias normais, para além de testing, é o comportamento esperado.

from lilya.apps import Lilya
from lilya.compat import reverse
from lilya.routing import Path


def user(): ...


app = Lilya(
    routes=[
        Path("/user", user, name="user"),
    ]
)

# Path lookup here
path = reverse("user")

# Reverse with a specific app
# Path lookup here
path = reverse("user", app=app)

O reverse também permite parâmetros de caminho.

from lilya.apps import Lilya
from lilya.compat import reverse
from lilya.requests import Request
from lilya.routing import Include, Path


def detail(): ...


app = Lilya(
    routes=[
        Include(
            "/users",
            routes=[
                Path("/{username}", detail, name="detail"),
            ],
            name="users",
        )
    ]
)

request = Request(...)

# Path lookup here
path = reverse(
    "users:detail",
    path_params={"username": ...},
)

# Reverse with a specific app
# Path lookup here
path = reverse(
    "users:detail",
    app=app,
    path_params={"username": ...},
)