Ir para o conteúdo

Responses

O Lilya por defeito, fornece responses específicas que servem um propósito duplo. Elas oferecem utilidade e são responsáveis por enviar as mensagens ASGI apropriadas através do canal send.

Lilya inclui automaticamente os cabeçalhos Content-Length e Content-Type.

Como funciona

Existem algumas formas de usar as respostas numa aplicação Lylia.

Respostas disponíveis

Todas as respostas do Lilya herdam do objecto Response e essa mesma classe também pode ser usada diretamente.

Todas as respostas são consideradas aplicações ASGI, o que significa que pode tratá-las como tal na sua aplicação, se necessário.

Exemplo

from lilya.responses import PlaiText
from lilya.types import Scope, Receive, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send):
    assert scope['type'] == 'http'
    response = PlaiText('Welcome')
    await response(scope, receive, send)

Response

from lilya.responses import Response

Exemplo

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


def home():
    return Response("Welcome home")


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

Lilya fornece o set_cookie que permite definir um cookie numa determinada resposta. Todas as respostas disponíveis no Lilya têm acesso a essa funcionalidade.

from lilya.responses import Response
from lilya.types import Scope, Receive, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send):
    assert scope['type'] == 'http'
    response = Response('Welcome', media_type='text/plain')

    response.set_cookie(key=..., value=..., max_age=..., expires=...,)
    await response(scope, receive, send)
Parâmetros

Os parâmetros disponíveis do set_cookie são os seguintes:

  • key - Uma string que representa a chave da cookie.
  • value - Uma string que representa o valor da cookie.
  • max_age - Um número inteiro que define o tempo de vida útil da cookie em segundos. Um valor negativo ou 0 descarta a cookie imediatamente. (Opcional)
  • expires - Um número inteiro que indica os segundos até que a cookie expire ou um objecto datetime. (Opcional)
  • path - Uma string a especificar o subconjunto de rotas a que a cookie se aplica. (Opcional)
  • domain - Uma string a especificar o domínio válido para a cookie. (Opcional)
  • secure - Um booleano que indica que a cookie é enviada para o servidor apenas se o pedido usa SSL e o protocolo HTTPS. (Opcional)
  • httponly - Um booleano que indica que a cookie não é acessível via JavaScript por meio de Document.cookie, XMLHttpRequest ou APIs de pedido. (Opcional)
  • samesite - Uma string a especificar a estratégia samesite para a cookie, com valores válidos de 'lax', 'strict' e 'none'. Padrão é 'lax'. (Opcional)

Da mesma forma que o definir cookie, esta função está disponível em todas as respostas fornecidas pelo Lilya.

from lilya.responses import Response
from lilya.types import Scope, Receive, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send):
    assert scope['type'] == 'http'
    response = Response('Welcome', media_type='text/plain')

    response.delete_cookie(key=..., path=..., domain=...)
    await response(scope, receive, send)
Parâmetros

Os parâmetros disponíveis do set_cookie são os seguintes:

  • key - Uma string que representa a chave da cookie.
  • path - Uma string a especificar o subconjunto de rotas a que a cookie se aplica. (Opcional)
  • domain - Uma string a especificar o domínio válido para a cookie. (Opcional)

HTMLResponse

Retorna uma resposta html.

from lilya.responses import HTMLResponse

Exemplo

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


def home():
    return HTMLResponse("<html><body><p>Welcome!</p></body></html>")


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

Error

Resposta que pode ser usada ao lançar um erro 500. Padrão é retornar uma resposta html.

from lilya.responses import Error

Exemplo

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


def home():
    return Error("<html><body><p>Error!</p></body></html>")


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

PlainText

Resposta que pode ser usada para retornar text/plain.

from lilya.responses import PlainText

Exemplo

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


def home():
    return PlainText("Welcome home")


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

JSONResponse

Resposta que pode ser usada para retornar application/json.

from lilya.responses import JSONResponse

Exemplo

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


def home():
    return JSONResponse({"message": "Welcome home"})


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

Ok

Resposta que pode ser usada para retornar application/json também. Pode ver isto como uma alternativa ao JSONResponse.

from lilya.responses import Ok

Exemplo

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


def home():
    return Ok({"message": "Welcome home"})


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

RedirectResponse

Usado para redirecionar as respostas.

from lilya.responses import RedirectResponse

Exemplo

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


def home():
    return RedirectResponse(url="/another-url")


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

StreamingResponse

from lilya.responses import StreamingResponse

Exemplo

from collections.abc import Generator

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


def my_generator() -> Generator[str, None, None]:
    count = 0
    while True:
        count += 1
        yield str(count)


def home():
    return StreamingResponse(my_generator(), media_type="text/html")


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

FileResponse

from lilya.responses import FileResponse

Envia um ficheiro de forma assíncrona como resposta, empregando um conjunto distinto de argumentos para a instância em comparação com outros tipos de resposta:

  • path - O caminho do ficheiro a ser transmitido.
  • status_code - O código de status a ser retornado.
  • headers - Cabeçalhos personalizados a serem incluídos, fornecidos como um dicionário.
  • media_type - Uma string a especificar o tipo de mídia. Se não especificado, o nome do ficheiro ou caminho é usado para deduzir o tipo de mídia.
  • filename - Se especificado, incluído no Content-Disposition da resposta.
  • content_disposition_type - Incluído no Content-Disposition da resposta. Pode ser definido como attachment (padrão) ou inline.
  • background - Uma instância de tarefa.

Exemplo

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


def home():
    return FileResponse(
        "files/something.csv",
        filename="something",
    )


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

Importando a classe apropriada

Esta é a forma clássica mais usada de usar as respostas. As respostas disponíveis contêm uma lista de respostas disponíveis no Lilya, mas também é livre para criar as suas próprias e usá-las.

Exemplo

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


def home():
    return JSONResponse({"message": "Welcome home"})


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

Construir a resposta

Aqui é onde as coisas ficam ótimas. O Lilya fornece uma função make_response que automaticamente construirá a resposta.

from lilya.responses import make_response

Exemplo

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


def home():
    return make_response({{"message": "Hello"}}, status_code=201)


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

Por defeito, o make_response retorna um JSONResponse, mas isso também pode ser alterado se o parâmetro response_class for definido como outra coisa.

Então, por que é que o make_response é diferente das outras respostas? Bem, aqui é onde Lilya brilha.

O Lilya é puramente Python, o que significa que não depende de bibliotecas externas como Pydantic, msgspec, attrs ou qualquer outra mas permite que se construa um codificador personalizado que pode ser usado posteriormente para serializar a resposta automaticamente e depois passá-la para o make_response.

Verifique a secção construir um codificador personalizado e codificadores personalizados com make_response para mais detalhes e como aproveitar o poder do Lilya.

Delegar para Lilya

Delegar para Lilya significa que, se nenhuma resposta for especificada, Lilya passará pelos codificadores internos e tentará jsonificar a resposta.

Vejamos um exemplo.

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


def home():
    return {"message": "Welcome home"}


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

Como pode ver, nenhuma resposta foi especificada, mas em vez disso, um dicionário Python foi retornado. O que Lilya faz internamente é adivinhar e entender o tipo de resposta, analisar o resultado em json e retornar automaticamente um JSONResponse.

Se o tipo de resposta não for serializável em json, então um ValueError é lançado.

Vejamos mais alguns exemplos.

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


def home_dict():
    return {"message": "Welcome home"}


def home_frozen_set():
    return frozenset({"message": "Welcome home"})


def home_set():
    return set({"message": "Welcome home"})


def home_list():
    return ["Welcome", "home"]


def home_str():
    return "Welcome home"


def home_int():
    return 1


def home_float():
    return 2.0


app = Lilya(
    routes=[
        Path("/dict", home_dict),
        Path("/fronzenset", home_frozen_set),
        Path("/set", home_set),
        Path("/list", home_list),
        Path("/str", home_str),
        Path("/int", home_int),
        Path("/float", home_float),
    ]
)

E a lista continua. O Lilya, por design, entende quase todas as estruturas de dados do Python por defeito, incluindo Enum, deque, dataclasses, PurePath, generators e tuple.

Codificadores padrão

Para entender como serializar um objecto específico em json, o Lilya possui alguns codificadores padrão que são avaliados quando tenta adivinhar o tipo de resposta.

  • DataclassEncoder - Serializa objectos dataclass.
  • EnumEncoder - Serializa objectos Enum.
  • PurePathEncoder - Serializa objectos PurePath.
  • PrimitiveEncoder - Serializa tipos primitivos do Python. str, int, float e None.
  • DictEncoder - Serializa tipos dict.
  • StructureEncoder - Serializa tipos de dados mais complexos. list, set, frozenset, GeneratorType, tuple, deque.

E quando um novo codificador é necessário e não é suportado nativamente pelo Lilya? Bem, construir um codificador personalizado é extremamente fácil e possível.

Construir um codificador personalizado

Como mencionado antes, o Lilya possui codificadores padrão que são usados para transformar uma resposta numa resposta serializável em json.

Para criar um codificador personalizado, deve usar a classe Encoder do Lilya e substituir a função serialize() onde ocorre o processo de serialização do tipo de codificador.

Em seguida, deve registrar o codificador para que o Lilya o possa usar.

Ao definir um codificador, o __type__ ou def is_type(self, value: Any) -> bool: deve ser declarado ou substituído.

Quando o __type__ é declarado corretamente, o is_type padrão avaliará o objecto em relação ao tipo e retornará True ou False.

Isto é usado internamente para perceber o tipo de codificador que será aplicado a um determinado objecto.

Warning

Se não puder fornecer o __type__ por qualquer motivo e quiser apenas substituir a avaliação padrão, simplesmente substitua o is_type() e aplique sua lógica personalizada lá.

Por exemplo: No Python 3.8, para um modelo Pydantic BaseModel se passado no __type__, ele lançará um erro devido a questões internas do Pydantic, então, para contornar esse problema, pode simplesmente substituir o is_type() e aplicar a lógica que valida o tipo do objecto e retorna um booleano.

from lilya.encoders import Encoder, register_encoder

Exemplo

Criar e registrar um codificador que lida com tipos msgspec.Struct.

from typing import Any

import msgspec
from msgspec import Struct

from lilya.encoders import Encoder, register_encoder


class MsgSpecEncoder(Encoder):
    __type__ = Struct

    def serialize(self, obj: Any) -> Any:
        """
        When a `msgspec.Struct` is serialised,
        it will call this function.
        """
        return msgspec.json.decode(msgspec.json.encode(obj))

    def encode(
        self,
        structure: Any,
        obj: Any,
    ) -> Any:
        return msgspec.json.decode(obj, type=structure)


# A normal way
register_encoder(MsgSpecEncoder())

# As alternative
register_encoder(MsgSpecEncoder)

Simples, certo? Porque agora o MsgSpecEncoder está registrado, pode simplesmente fazer isto nos handlers e retornar diretamente o tipo de objecto msgspec.Struct.

from msgspec import Struct

from lilya.routing import Path


class User(Struct):
    name: str
    email: str


def msgspec_struct():
    return User(name="lilya", url="example@lilya.dev")

Criar codificadores personalizados específicos

Lilya sendo 100% Python puro e não vinculado a nenhuma biblioteca de validação específica permite que crie codificadores personalizados que são posteriormente usados pelas respostas do Lilya.

Ok, isso parece um bocado confuso, certo? Aposto que sim, então vamos devagar.

Imagine que quer usar uma biblioteca de validação específica, como Pydantic, msgspec ou até mesmo attrs ou qualquer outra à sua escolha.

Quer ter certeza de que, se retornar um modelo Pydantic ou uma msgspec Struct ou até mesmo uma classe define attr.

Vejamos como seria para cada um deles.

Para Pydantic BaseModel

from __future__ import annotations

from typing import Any

from pydantic import BaseModel

from lilya.encoders import Encoder, register_encoder


class PydanticEncoder(Encoder):
    __type__ = BaseModel
    # optional a name can be provided, so same named encoders are removed
    name = "ModelDumpEncoder"

    # is_type and is_type_structure are provided by Encoder.
    # checked is the type provided by __type__.

    def serialize(self, obj: BaseModel) -> dict[str, Any]:
        return obj.model_dump()

    def encode(self, structure: type[BaseModel], value: Any) -> Any:
        return structure(**value)


# A normal way
register_encoder(PydanticEncoder())

# As alternative
register_encoder(PydanticEncoder)

Para msgspec Struct

from typing import Any

import msgspec
from msgspec import Struct

from lilya.encoders import Encoder, register_encoder


class MsgSpecEncoder(Encoder):
    __type__ = Struct

    def serialize(self, obj: Any) -> Any:
        """
        When a `msgspec.Struct` is serialised,
        it will call this function.
        """
        return msgspec.json.decode(msgspec.json.encode(obj))

    def encode(
        self,
        structure: Any,
        obj: Any,
    ) -> Any:
        return msgspec.json.decode(obj, type=structure)


# A normal way
register_encoder(MsgSpecEncoder())

# As alternative
register_encoder(MsgSpecEncoder)

Para attrs

from typing import Any

from attrs import asdict, has

from lilya.encoders import Encoder, register_encoder


class AttrsEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        """
        You can use this function instead of declaring
        the `__type__`.
        """
        return has(value)

    def serialize(self, obj: Any) -> Any:
        return asdict(obj)

    def encode(self, structure: type[Any], obj: Any) -> Any:
        if isinstance(obj, dict):
            return structure(**obj)
        return structure(*obj)



# A normal way
register_encoder(AttrsEncoder())

# As alternative
register_encoder(AttrsEncoder)

Fácil e poderoso, certo? Sim.

Entende o que isso significa? Significa que pode criar qualquer codificador à sua escolha usando também qualquer biblioteca à escolha.

A flexibilidade do Lilya permite que seja livre e para que o Lilya não esteja vinculado a nenhuma biblioteca específica.

Codificadores personalizados e respostas

Depois que os codificadores personalizados nos exemplos forem criados, isto permite fazer algo diretamente assim.

from attrs import define
from msgspec import Struct
from pydantic import BaseModel

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


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


class Item(Struct):
    name: str
    age: int


@define
class AttrItem:
    name: str
    age: int


def pydantic_response():
    return User(name="lilya", age=24)


def pydantic_response_list():
    return [User(name="lilya", age=24)]


def msgspec_struct():
    return Item(name="lilya", age=24)


def msgspec_struct_list():
    return [Item(name="lilya", age=24)]


def attrs_response():
    return AttrItem(name="lilya", age=24)


def attrs_response_list():
    return [AttrItem(name="lilya", age=24)]


app = Lilya(
    routes=[
        Path("/pydantic", pydantic_response),
        Path("/pydantic-list", pydantic_response_list),
        Path("/msgspec", msgspec_struct),
        Path("/msgspec-list", pydantic_response_list),
        Path("/attrs", attrs_response),
        Path("/attrs-list", attrs_response_list),
    ]
)

Codificadores personalizados e o make_response

Bem, aqui é onde o make_response o ajuda. O make_response irá gerar um JSONResponse por defeito e quando retornar um tipo de codificador personalizado, existem algumas limitações para isso.

Por exemplo, e se quiser retornar com um status_code diferente? Ou até mesmo adicionar-lhe uma tarefa?

O codificador personalizado não lida com isso para si, mas o make_response lida!

Vejamos como ficaria agora utilizando o make_response.

from attrs import define
from msgspec import Struct
from pydantic import BaseModel

from lilya import status
from lilya.apps import Lilya
from lilya.responses import make_response
from lilya.routing import Path


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


class Item(Struct):
    name: str
    age: int


@define
class AttrItem:
    name: str
    age: int


def pydantic_response():
    data = User(name="lilya", age=24)
    return make_response(
        data,
        status_code=status.HTTP_200_OK,
    )


def pydantic_response_list():
    data = [User(name="lilya", age=24)]
    return make_response(
        data,
        status_code=status.HTTP_201_CREATED,
        background=...,
        headers=...,
    )


def msgspec_struct():
    return make_response(Item(name="lilya", age=24))


def msgspec_struct_list():
    return make_response(
        [Item(name="lilya", age=24)],
        status_code=...,
    )


def attrs_response():
    return make_response(
        AttrItem(name="lilya", age=24),
        status_code=...,
    )


def attrs_response_list():
    return make_response(
        [AttrItem(name="lilya", age=24)],
        status_code=...,
    )


app = Lilya(
    routes=[
        Path("/pydantic", pydantic_response),
        Path("/pydantic-list", pydantic_response_list),
        Path("/msgspec", msgspec_struct),
        Path("/msgspec-list", pydantic_response_list),
        Path("/attrs", attrs_response),
        Path("/attrs-list", attrs_response_list),
    ]
)