Contexto
Uma implementação recente no asyncworker (doc) permitiu que um handler http pudesse receber parâmetros mais complexos do que simplesmente Request
, que é o que normalmente um handler http recebe.
Essa implementação permite que possíveis valores possam ser acumulados ao longo da execução do código e no momento que o handler for chamado para atender o Request
atual, esses valores seriam consultados para saber se o handler está interessado em receber algum deles.
Esse “interesse” é demonstrado pelo handler através da sua própria assinatura python.
O mais legal nesse caso é que tudo isso acaba sendo código python sintaticamente válido. Nesse post usamos o asyncworker 0.11.0
.
Vejamos um exemplo simples.
Exemplo de um handler HTTP
Quando falamos de framework HTTP geralmente as implementações dos endpoints recebem um objeto Request
e retornam um objeto Response
.
Definição do handler
Vamos escrever um handler http que apenas recebe um JSON no corpo do request e retorna esse json.
O que vamos fazer aqui é escrever código para que nosso handler não precise receber Request
e possa receber algo mais rico e já validado, como por exemplo um objeto que já represente o corpo do request.
Handler recebendo Request
Aqui temos a implementação padrão do handler, recebendo Request
e pegando desse request os dados necessários.
Códigos fornecidos pelo asyncworker para podermos chamar um handler com assinatura dinâmica
O asyncworker fornece uma infra-estrutura de código que ajuda nessa parte de poder ter um handler com a assinatura dinâmica, ou seja, o handler pode ter uma assinatura mais complexa do que um simples (request: Request)
e podemos inclusive mudar essa assinatura ao longo do tempo sem precisarmos mudar nenhuma linha de código, a não ser a própria linha da assinatura. Veremos exemplos disso mais adiante.
Essa infra-estrutura é dividida em algumas partes:
TypesRegistry
;- Uma instancia de
TypesResgitry
presente em cadaRequest
que chega; - A corotina
call_http_handler(request, handler)
que é capaz de analisar a assinatura de uma handler, consultar a instância deTypesRegistry
que aRequest
possui e chamar o handler com os parametros corretos.
O TypesRegistry possui uma interface bem simples para que possamos ir adicionando valores de tipos quaisquer. Tem apenas dois métodos: set()
e get()
.
O objeto Request
original também é modificado e recebe uma instância desse TypesRegistry
. Isso é feito automaticamente pelo asyncworker, mesmo que você não faça uso dessa estrutura de chamada dinâmica.
A última parte dessa estrutura de código é a função call_http_handler()
. Com ela é possível passar o request original e o handler a ser chamado. Dessa forma o handler terá sua assinatura analisada e será chamado com os parametros corretos.
Exemplo de decorator que faz uso dessa estrutura para chamar um handler
O que faremos aqui é apenas um decorator “oco”, mas que já faz uso dessa estrutura que mencionamos. Do ponto de vista do código do handler nada muda, ele vai continuar recebendo Request
, mas toda a estrutura para que ele possa receber mais do que isso já estará iniciada.
Veja que esse decorator não faz nada demais e o handler continua recebendo o Request
. Mas podemos começar e modificá-lo para que o handler receba parametros mais ricos.
Modelo que representa o corpo do request
Vamos definir o modelo que vai representar o corpo do request, que será o UserResource
. Usaremos pydantic para modelar esse objeto.
É um modelo bem simples e vai ser mesmo apenas para validarmos o que estamos falando, mas lembre-se que esse modelo pode ser arbtitrariamente complexo.
Modificando nosso decorator para pegar o corpo do request e construir um modelo UserResource
A partir de agora nosso request já possui um objeto do tipo UserResource
dentro do seu TypesRegistry
e esse objeto já pode ser passado para quaisquer handlers que se interessem por esse tipo. Então podemos mudar a assinatura do nosso handler.
Mudando a assinatura do handler para receber UserResource
Apenas para relembrar, esse era o código original do nosso handler:
E esse é o novo código do handler:
Aqui temos o handler já recebendo o corpo do request validado. O decorator parse_body
pode, por exemplo, já retornar HTTP 400
caso o corpo do request não esteja em conformidade com o modelo UserResource
. Pode também ser modificado para que fique mais genérico, podendo ser reusado em múltiplos handlers, algo nessa linha:
@app.route(["/posts"], type=RouteTypes.HTTP, methods=["POST"])
@parse_body(PostResource)
async def users(post: PostResource):
...
Versão final do código
Esse é o código em sua forma final, já com o handler usando o decorator e tendo sua assinatura recebendo um modelo já validado:
Exemplos adicionais de decorators
Um outro exemplo é um decorator de autenticação que pode fornecer por exemplo um objeto AuthenticatedUser
. Um handler ficaria com o código nessa linha:
@app.route(["/users"], type=RouteTypes.HTTP, methods=["POST"])
@auth
@parse_body
async def users(body: UserResource):
return json_response(body.dict())
O que o decorator auth
faz é:
- Analisar o request que está sendo atendido;
- Checar se os dados de autenticação estão corretos;
- Buscar os dados do usuário autenticado (ou já retornar
HTTP 401
); - Adicionar ao
TypesRegsitry
desse request uma instância deAuthenticatedUser
.
O que isso significa? Significa que esse handler está agora exigindo um request autenticado mas não necessáriamente está interessado em saber qual usuário está fazendo o request.
O mais legal é: Se a partir de algum momento o handler quiser saber quem é o usuário autenticado basta mudar a assinatura para:
async def users(body: UserResource, user: AuthenticatedUser):
return json_response(user.dict())
Aqui não importa a ordem os parametros, a análise da assinatura será feita da mesma forma e o handler será chamado.
Futuro
O que vimos aqui é um código ainda experimental e que acabou de ser criado então ainda existem alguns passos que temos que fazer manualmente, como por exemplo a implementação dos decorators mostrados aqui. Fazer com decorators é apenas uma forma de fazer.
Podemos por exemplo implementar o decorator auth
em forma de um middleware, que pode eventualmente ser aplicado a um grupo de handlers.
Um outra ideia também é embutir mais lógica no asyncworker para que cada vez menos decorators/middlewares sejam necessários. Um exemplo simples é o próprio parsing do body do request. Em vez de termos esse decorator parse_body
o próprio asyncworker poderia fornecer anotações de tipo para isso, assim:
from asyncworker.types import RequestBody
@app.route(["/users"], type=RouteTypes.HTTP, methods=["POST"])
async def users(body: RequestBody[UserResource]):
return json_response(body.dict())
O mais interessante disso é que parte das regras do sistema (parsing do corpo do request, nesse caso) é descrita com código python válido. Isso significa que não é possível seu código estar escrito de uma forma e se comportar de outra.
Como essas anotações podem ser analisadas dinamicamente podemos, por exemplo, gerar documentaação de forma automática sem que essa documentação fique defasada em relação ao código, afinal é o próprio código quem diz como a documenação será criada.
O que fica aqui são algumas ideias de implementação para o projeto asyncworker. Se você gostou, estamos sempre interessados em receber contribuições para o projeto, tanto código quanto discussão de ideias para o futuro.
O projeto está no github: https://github.com/B2W-BIT/async-worker