Conversores de caminho Django 2.0+

Olá!



O roteamento em Django da segunda versão do framework recebeu uma ferramenta maravilhosa - conversores. Com a adição desta ferramenta, tornou-se possível não só configurar com flexibilidade os parâmetros nas rotas, mas também separar as áreas de responsabilidade dos componentes.



Meu nome é Alexander Ivanov, sou um mentor no Yandex.Practicum na faculdade de desenvolvimento de back- end e um desenvolvedor líder no Laboratório de Modelagem de Computadores. Neste artigo, vou guiá-lo pelos conversores de rota do Django e mostrar os benefícios de usá-los. A primeira coisa para começar são os limites de aplicabilidade:











  1. Django versão 2.0+;
  2. o registro das rotas deve ser feito com django.urls.path



    .


Portanto, quando uma solicitação chega ao servidor Django, ela primeiro passa pela cadeia de middleware e, em seguida, o URLResolver ( algoritmo ) é ativado . A tarefa deste último é encontrar um adequado na lista de rotas cadastradas.



Para uma análise substantiva, proponho considerar a seguinte situação: existem vários endpoints que devem gerar relatórios diferentes para uma determinada data. Vamos supor que os endpoints sejam assim:



users/21/reports/2021-01-31/
teams/4/reports/2021-01-31/
      
      







Quais seriam as rotas urls.py



? Por exemplo, assim:



path('users/<id>/reports/<date>/', user_report, name='user_report'),
path('teams/<id>/reports/<date>/', team_report, name='team_report'),
      
      





Cada item em < >



é um parâmetro de solicitação e será passado para o manipulador.

Importante: o nome do parâmetro ao registrar a rota e o nome do parâmetro no manipulador devem corresponder.


Então, cada manipulador teria algo assim (preste atenção às anotações de tipo):



def user_report(request, id: str, date: str):
   try:
       id = int(id)
       date = datetime.strptime(date, '%Y-%m-%d')
   except ValueError:
       raise Http404()
  
   # ...
      
      





Mas este não é um negócio real - copiar e colar tal bloco de código para cada manipulador. É razoável mover este código para uma função auxiliar:



def validate_params(id: str, date: str) -> (int, datetime):
   try:
       id = int(id)
       date = datetime.strptime(date, '%Y-%m-%d')
   except ValueError:
       raise Http404('Not found')
   return id, date
      
      





E em cada manipulador, haverá uma chamada simples para esta função auxiliar:



def user_report(request, id: str, date: str):
   id, date = validate_params(id, date)
  
   # ...
      
      





Em geral, isso já é digerível. A função auxiliar irá retornar os parâmetros corretos dos tipos necessários ou abortar o manipulador. Tudo parece estar bem.



Mas, na verdade, aqui está o que eu fiz: mudei parte da responsabilidade de decidir se esse manipulador deve ser executado para esta rota ou não, do URLResolver para o próprio manipulador. Acontece que o URLResolver fez seu trabalho mal, e meus gerenciadores não só precisam fazer um trabalho útil, mas também decidir se devem fazê-lo. Esta é uma violação clara do princípio SOLID de responsabilidade exclusiva . Isso não vai funcionar. Precisamos melhorar.



Conversores padrão



Django fornece conversores de rota padrão . É um mecanismo para determinar se uma parte da rota é apropriada ou não pelo próprio URLResolver. Um bom bônus: o conversor pode alterar o tipo do parâmetro, o que significa que o tipo de que precisamos pode vir imediatamente para o manipulador, e não para a string.



Os conversores são especificados antes do nome do parâmetro na rota, separados por dois pontos. Na verdade, todos os parâmetros têm um conversor; se não for especificado explicitamente, o conversor será usado por padrão str



.



Cuidado: alguns conversores parecem tipos em Python, então pode parecer que são casts normais, mas não são - por exemplo, não há conversores padrão float



ou bool



. Mais tarde, vou mostrar o que é um conversor.




Depois de observar os conversores padrão, fica óbvio para que id



usar o conversor int



:



path('users/<int:id>/reports/<date>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date>/', team_report, name='team_report'),
      
      







Mas e a data? Não existe um conversor padrão para ele.



Você pode, é claro, se esquivar e fazer isso:



'users/<int:id>/reports/<int:year>-<int:month>-<int:day>/'

      
      





Na verdade, alguns dos problemas foram eliminados, pois agora é garantido que a data será exibida em três números separados por hífens. No entanto, você ainda terá que lidar com as pastas de problemas no manipulador se o cliente enviar uma data incorreta, por exemplo, 2021-02-29 ou 100-100-100 em geral. Isso significa que essa opção não é adequada.



Nós criamos nosso próprio conversor



O Django, além dos conversores padrão, oferece a capacidade de criar seu próprio conversor e descrever as regras de conversão como você quiser.



Para fazer isso, você precisa seguir duas etapas:



  1. Descreva a classe do conversor.
  2. Registre o conversor.


Uma classe conversora é uma classe com um certo conjunto de atributos e métodos descritos na documentação (em minha opinião, é um pouco estranho que os desenvolvedores não tenham feito uma classe abstrata base). Os próprios requisitos:



  1. Deve haver um atributo que regex



    descreve a expressão regular para encontrar rapidamente a subsequência necessária. Vou mostrar como ele é usado mais tarde.
  2. Implemente um método def to_python(self, value: str)



    para converter de uma string (afinal, a rota transmitida é sempre uma string) em um objeto Python, que será passado ao manipulador.
  3. Implemente um método def to_url(self, value) -> str



    para converter de volta de um objeto Python em uma string (usado ao invocar django.urls.reverse



    ou marcar url



    ).


A classe para converter a data ficará assim:



class DateConverter:
   regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'

   def to_python(self, value: str) -> datetime:
       return datetime.strptime(value, '%Y-%m-%d')

   def to_url(self, value: datetime) -> str:
       return value.strftime('%Y-%m-%d')
      
      





Sou contra a duplicação, por isso colocarei o formato da data em um atributo - é mais fácil manter o conversor se de repente eu quiser (ou precisar) alterar o formato da data:



class DateConverter:
   regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'
   format = '%Y-%m-%d'

   def to_python(self, value: str) -> datetime:
       return datetime.strptime(value, self.format)

   def to_url(self, value: datetime) -> str:
       return value.strftime(self.format)
      
      





A classe está descrita, então é hora de registrá-la como um conversor. Isso é feito de forma muito simples: na função register_converter



você precisa especificar a classe descrita e o nome do conversor para usá-lo nas rotas:



from django.urls import register_converter
register_converter(DateConverter, 'date')
      
      





Agora você pode descrever as rotas em urls.py



(alterei deliberadamente o nome do parâmetro para dt



a fim de não confundir a entrada date:date



):



path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date:dt>/', team_report, name='team_report'),
      
      





Agora é garantido que os handlers serão chamados apenas se o conversor funcionar corretamente, o que significa que os parâmetros do tipo requerido chegarão ao handler:



def user_report(request, id: int, dt: datetime):
   #     
   #      
      
      





Parece incrível! E é assim, você pode verificar.



Sob o capô



Se você olhar de perto, surge uma questão interessante: em nenhum lugar há uma verificação de que a data está correta. Sim, existe uma temporada regular, mas uma data incorreta também é adequada para ela, por exemplo 2021-01-77, o que significa que to_python



deve haver um erro nela. Por que isso funciona?



Sobre isso eu digo: "Jogue de acordo com as regras do framework, e ele vai jogar para você." Frameworks assumem uma série de tarefas comuns. Se a estrutura não pode fazer algo, uma boa estrutura oferece uma oportunidade de expandir sua funcionalidade. Portanto, você não deve se envolver na construção de bicicletas, é melhor ver como a estrutura oferece para melhorar suas próprias capacidades.



Django tem um subsistema de roteamento com a capacidade de adicionar conversores que cuidam da chamada do método to_python



e captura de erros ValueError



.



Aqui está o código do subsistema de roteamento Django sem mudanças (versão 3.1, arquivo django/urls/resolvers.py



, classe RoutePattern



, método match



):



match = self.regex.search(path)
if match:
   # RoutePattern doesn't allow non-named groups so args are ignored.
   kwargs = match.groupdict()
   for key, value in kwargs.items():
       converter = self.converters[key]
       try:
           kwargs[key] = converter.to_python(value)
       except ValueError:
           return None
   return path[match.end():], (), kwargs
return None

      
      





O primeiro passo é procurar correspondências na rota transmitida do cliente usando uma expressão regular. Aquele regex



que é definido na classe conversora participa da formação self.regex



, ou seja, é substituído em vez da expressão entre colchetes <>



na rota.



Por exemplo,
users/<int:id>/reports/<date:dt>/
      
      



transformar-se em

^users/(?P<id>[0-9]+)/reports/(?P<dt>[0-9]{4}-[0-9]{2}-[0-9]{2})/$
      
      





No final, apenas o mesmo de regular DateConverter



.



Esta é uma pesquisa rápida, superficial. Se nenhuma correspondência for encontrada, a rota definitivamente não é adequada, mas, se encontrada, é uma rota potencialmente adequada. Isso significa que você precisa iniciar o próximo estágio de verificação.



Cada parâmetro possui seu próprio conversor, que é usado para chamar o método to_python



. E aqui está o mais interessante: a chamada é to_python



encerrada try/except



e os erros de tipo são detectados ValueError



. É por isso que o conversor funciona mesmo no caso de uma data incorreta: cai um erro ValueError



, e isso é considerado para que a rota não cabe.



Então, no caso de DateConverter



, podemos dizer, sorte: no caso de uma data incorreta, cai um erro do tipo exigido. Se houver um erro de outro tipo, o Django retornará uma resposta 500.



Não pare



Parece que está tudo bem, os conversores estão funcionando, os tipos necessários vêm imediatamente para os manipuladores ... Ou não na hora?



path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
      
      





No manipulador para gerar um relatório, você provavelmente precisa dele User



, e não dele id



(embora possa ser o caso). Na minha situação hipotética, apenas um objeto é necessário para criar um relatório User



. O que acontece então, novamente vinte e cinco?



def user_report(request, id: int, dt: datetime):
   user = get_object_or_404(User, id=id)
  
   # ...
      
      





Transferindo responsabilidades para o manipulador novamente.



Mas agora está claro o que fazer com ele: escreva seu próprio conversor! Ele irá certificar-se de que o objeto existe User



e irá passá-lo ao manipulador.



class UserConverter:
   regex = r'[0-9]+'

   def to_python(self, value: str) -> User:
       try:
           return User.objects.get(id=value)
       except User.DoesNotExist:
           raise ValueError('not exists') #  ValueError

   def to_url(self, value: User) -> str:
       return str(value.id)
      
      





Depois de descrever a aula, eu a registro:



register_converter(UserConverter, 'user')
      
      





Finalmente, descrevo a rota:



path('users/<user:u>/reports/<date:dt>/', user_report, name='user_report'),
      
      





Isso é melhor:



def user_report(request, u: User, dt: datetime):  
   # ...
      
      





Conversores para modelos podem ser usados ​​com frequência, por isso é conveniente fazer a classe base de tal conversor (ao mesmo tempo, adicionei uma verificação para a existência de todos os atributos):



class ModelConverter:
   regex: str = None
   queryset: QuerySet = None
   model_field: str = None

   def __init__(self):
       if None in (self.regex, self.queryset, self.model_field):
           raise AttributeError('ModelConverter attributes are not set')

   def to_python(self, value: str) -> models.Model:
       try:
           return self.queryset.get(**{self.model_field: value})
       except ObjectDoesNotExist:
           raise ValueError('not exists')

   def to_url(self, value) -> str:
       return str(getattr(value, self.model_field))

      
      





Em seguida, a descrição do novo conversor para o modelo será reduzida a uma descrição declarativa:



class UserConverter(ModelConverter):
   regex = r'[0-9]+'
   queryset = User.objects.all()
   model_field = 'id'

      
      





Resultado



Os conversores de rota são um mecanismo poderoso que ajuda a tornar seu código mais limpo. Mas esse mecanismo apareceu apenas na segunda versão do Django - antes disso, tínhamos que fazer sem ele. É get_object_or_404



daí que vêm as funções auxiliares do tipo ; sem esse mecanismo, bibliotecas legais como o DRF são feitas.



Mas isso não significa que os conversores não devam ser usados. Isso significa que (ainda) não será possível usá-los em todos os lugares. Mas, sempre que possível, recomendo que você não os negligencie.



Deixarei uma ressalva: aqui é importante não exagerar e não arrastar o cobertor na outra direção - você não precisa levar a lógica de negócios para o conversor. É necessário responder à pergunta: se tal rota é em princípio impossível, então esta é a área de responsabilidade do conversor; se tal rota for possível, mas sob certas circunstâncias ela não for processada, então isso já é responsabilidade do manipulador, serializador ou de outra pessoa, mas definitivamente não do conversor.



PS Na prática, fiz e usei apenas um conversor de datas, apenas o mostrado no artigo, já que quase sempre uso DRF ou GraphQL. Informe-nos se você usa conversores de rota e, em caso afirmativo, quais?



All Articles