Ano de aventura com grafeno-python

Ano de aventura com grafeno-python


imagem


Oi pessoal, Sou desenvolvedor de python. No ano passado, trabalhei com grafeno-python + django ORM e, durante esse período, tentei criar algum tipo de ferramenta para tornar o trabalho com grafeno mais conveniente. Como resultado, recebi uma pequena base de código da graphene-framework e um conjunto de algumas regras que gostaria de compartilhar.


imagem


O que é grafeno-python?


De acordo com graphene-python.org , então:


Graphene-Python é uma biblioteca para criar facilmente APIs GraphQL usando Python. Sua principal tarefa é fornecer uma API simples, mas ao mesmo tempo extensível, para facilitar a vida dos programadores.

Sua principal tarefa é fornecer uma API simples, mas ao mesmo tempo extensível, para facilitar a vida dos programadores.


Sim, na realidade o grafeno é simples e extensível, mas me parece muito simples para aplicativos grandes e de crescimento rápido. Uma documentação curta (usei o código-fonte - é muito mais detalhado), além da falta de padrões para escrever código, essa biblioteca não é a melhor opção para sua próxima API.


Seja como for, eu decidi usá-lo no projeto e tive vários problemas, felizmente, por ter resolvido a maioria deles (graças aos ricos recursos não documentados do grafeno). Algumas de minhas soluções são puramente arquitetônicas e podem ser usadas imediatamente, sem a minha estrutura. No entanto, o restante deles ainda exige alguma base de código.


Este artigo não é documentação, mas, em certo sentido, uma breve descrição do caminho que segui e dos problemas que resolvi de uma maneira ou de outra, com uma breve justificativa para minha escolha. Nesta parte, prestei atenção a mutações e coisas relacionadas a elas.


O objetivo deste artigo é obter algum feedback significativo , portanto, aguardarei críticas nos comentários!


Nota: antes de continuar lendo o artigo, recomendo fortemente que você se familiarize com o que é o GraphQL.




Mutações


A maioria das discussões sobre o GraphQL se concentra na obtenção de dados, mas qualquer plataforma que se preze também exige uma maneira de modificar os dados armazenados no servidor.

Vamos começar com mutações.


Considere o seguinte código:


 class UpdatePostMutation(graphene.Mutation): class Arguments: post_id = graphene.ID(required=True) title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, post_id, title, content, image_urls, allow_comments, contact_email): errors = [] try: post = get_post_by_id(post_id) except PostNotFound: return UpdatePostMutation(ok=False, errors=['post_not_found']) if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if not is_email(contact_email): errors.append('contact_email_not_valid') if post.owner != info.context.user: errors.append('not_post_owner') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, title, content, image_urls, allow_comments, contact_email) return UpdatePostMutation(ok=bool(errors), errors=errors) 

UpdatePostMutation altera a postagem com o id fornecido, usando os dados transferidos e retorna erros se algumas condições não forem atendidas.


É necessário apenas olhar para este código, pois ele se torna visível por sua extensibilidade e falta de suporte devido a:


  1. Muitos argumentos da função mutate , cujo número pode aumentar mesmo que desejemos adicionar mais campos a serem editados.
  2. Para que as mutações tenham a mesma aparência no lado do cliente, elas devem retornar errors e ok , para que seu status e o que é sempre possível entender.
  3. Pesquise e recupere um objeto na função mutate . A função de mutação opera com jejum e, se não estiver lá, a mutação não deve ocorrer.
  4. Verificando permissões em uma mutação. A mutação não deve ocorrer se o usuário não tiver o direito de fazer isso (editar algumas postagens).
  5. Um primeiro argumento inútil (uma raiz que é sempre None para campos de nível superior, que é a nossa mutação).
  6. Um conjunto imprevisível de erros: se você não tiver código-fonte ou documentação, não saberá quais erros essa mutação pode retornar, uma vez que não são refletidos no esquema.
  7. Existem muitas verificações de erro de modelo que são realizadas diretamente no método mutate , o que envolve a alteração dos dados, em vez de uma variedade de verificações. O mutate ideal deve consistir em uma linha - uma chamada para a função de pós-edição.

Em resumo, o mutate deve modificar os dados , em vez de cuidar de tarefas de terceiros, como acessar objetos e validar entradas. Nosso objetivo é chegar a algo como:


  def mutate(post, info, input): post = Utils.update_post(post, **input) return UpdatePostMutation(post=post) 

Agora vamos ver os pontos acima.




Tipos personalizados


O campo de email é passado como uma sequência, enquanto é uma sequência de um formato específico . Cada vez que a API recebe um email, deve verificar sua correção. Portanto, a melhor solução seria criar um tipo personalizado.


 class Email(graphene.String): # ... 

Isso pode parecer óbvio, mas vale a pena mencionar.




Tipos de entrada


Use tipos de entrada para suas mutações. Mesmo que não estejam sujeitos a reutilização em outros lugares. Graças aos tipos de entrada, as consultas se tornam menores, o que as torna mais fáceis de ler e mais rápidas de escrever.


 class UpdatePostInput(graphene.InputObjectType): title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True) 

Para:


 mutation( $post_id: ID!, $title: String!, $content: String!, $image_urls: String!, $allow_comments: Boolean!, $contact_email: Email! ) { updatePost( post_id: $post_id, title: $title, content: $content, image_urls: $image_urls, allow_comments: $allow_comments, contact_email: $contact_email, ) { ok } } 

Depois:


 mutation($id: ID!, $input: UpdatePostInput!) { updatePost(id: $id, input: $input) { ok } } 

O código de mutação muda para:


 class UpdatePostMutation(graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, input, id): # ... if not errors: post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation(errors=errors) 



Classe de mutação base


Como mencionado no parágrafo 2, as mutações devem retornar errors e ok para que seu status e suas causas sempre possam ser entendidos . É bastante simples, criamos uma classe base:


 class MutationPayload(graphene.ObjectType): ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) query = graphene.Field('main.schema.Query', required=True) def resolve_ok(self, info): return len(self.errors or []) == 0 def resolve_errors(self, info): return self.errors or [] def resolve_query(self, info): return {} 

Algumas notas:


  • O método resolve_ok é resolve_ok , portanto, não precisamos calcular ok .
  • O campo de query é a Query raiz, que permite consultar dados diretamente dentro da solicitação de mutação (os dados serão solicitados após a conclusão da mutação).
     mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok query { profile { totalPosts } } } } 

Isso é muito conveniente quando o cliente atualiza alguns dados após a conclusão da mutação e não deseja solicitar ao responsável pelo retorno todo esse conjunto. Quanto menos código você escreve, mais fácil é manter. Eu peguei essa ideia daqui .


Com a classe de mutação base, o código se transforma em:


 class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) def mutate(_, info, input, id): # ... 



Mutações na raiz


Nosso pedido de mutação agora se parece com isso:


 mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok } } 

Contendo todas as mutações em um escopo global não é uma boa prática. Aqui estão algumas razões pelas quais:


  1. Com o crescente número de mutações, torna-se cada vez mais difícil encontrar a mutação necessária.
  2. Devido a um espaço para nome, é necessário incluir "o nome do seu módulo" no nome da mutação, por exemplo, update Post .
  3. Você deve passar o id como argumento para a mutação.

Eu sugiro o uso de mutações na raiz . Seu objetivo é resolver esses problemas, separando mutações em escopos separados e liberando mutações da lógica de acesso a objetos e direitos de acesso a eles.


A nova solicitação é assim:


 mutation($id: ID!, $input: PostUpdateInput!) { post(id: $id) { update(input: $input) { ok } } } 

Os argumentos da solicitação permanecem os mesmos. Agora, a função de mudança é "chamada" dentro da post , o que permite que a seguinte lógica seja implementada:


  1. Se o id não id passado para post , ele retornará {} . Isso permite que você continue executando mutações dentro dele. Usado para mutações que não exigem um elemento raiz (por exemplo, para criar objetos).
  2. Se id for passado, o elemento correspondente será recuperado.
  3. Se o objeto não for encontrado, None retornado e isso concluirá a solicitação, a mutação não será chamada.
  4. Se o objeto for encontrado, verifique os direitos do usuário para manipulá-lo.
  5. Se o usuário não tiver direitos, None retornado e a solicitação será concluída, a mutação não será chamada.
  6. Se o usuário tiver direitos, o objeto encontrado será retornado e a mutação o receberá como raiz - o primeiro argumento.

Assim, o código de mutação muda para:


 class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() def mutate(post, info, input): if post is None: return None errors = [] if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation(errors=errors) 

  • A raiz da mutação - o primeiro argumento - agora é um objeto do tipo Post , sobre o qual a mutação é realizada.
  • A verificação de autorização foi movida para o código de mutação raiz.

Código de mutação raiz:


 class PostMutationRoot(MutationRoot): class Meta: model = Post has_permission = lambda post, user: post.owner == user update = UpdatePostMutation.Field() 



Interface de erro


Para tornar previsível um conjunto de erros, eles devem ser refletidos no design.


  • Como as mutações podem retornar vários erros, os erros devem ser uma lista.
  • Como os erros são representados por tipos diferentes, uma Union específica Union erros deve existir para uma mutação específica.
  • Para que os erros permaneçam semelhantes, eles devem implementar a interface, vamos chamá-la de ErrorInterface . Deixe que ele contenha dois campos: ok e message .

Portanto, os erros devem ser do tipo [SomeMutationErrorsUnion]! . Todos os subtipos de SomeMutationErrorsUnion devem implementar ErrorInterface .


Temos:


 class NotAuthenticated(graphene.ObjectType): message = graphene.String(required=True, default_value='not_authenticated') class Meta: interfaces = [ErrorInterface, ] class TitleTooShort(graphene.ObjectType): message = graphene.String(required=True, default_value='title_too_short') class Meta: interfaces = [ErrorInterface, ] class TitleAlreadyTaken(graphene.ObjectType): message = graphene.String(required=True, default_value='title_already_taken') class Meta: interfaces = [ErrorInterface, ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [NotAuthenticated, TitleIsTooShort, TitleAlreadyTaken, ] 

Parece bom, mas há muito código. Usamos a metaclasse para gerar esses erros rapidamente:


 class PostErrors(metaclass=ErrorMetaclass): errors = [ 'not_authenticated', 'title_too_short', 'title_already_taken', ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [PostErrors.not_authenticated, PostErrors.title_too_short, PostErrors.title_already_taken, ] 

Adicione a declaração dos erros retornados à mutação:


 class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() errors = graphene.List(UpdatePostMutationErrors, required=True) def mutate(post, info, input): # ... 



Verifique se há erros


Parece-me que o método mutate não deve se preocupar com nada além de alterar os dados . Para conseguir isso, você precisa verificar se há erros no código para esta função.


Omitindo a implementação, eis o resultado:


 class UpdatePostMutation(DefaultMutation): class Arguments: input = UpdatePostInput() class Meta: root_required = True authentication_required = True #   ,    True   # An iterable of tuples (error_class, checker) checks = [ ( PostErrors.title_too_short, lambda post, input: len(input.title) < TITLE_MIN_LENGTH ), ( PostErrors.title_already_taken, lambda post, input: Post.objects.filter(title=input.title).exists() ), ] def mutate(post, info, input): post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation() 

Antes de iniciar a função mutate , cada verificador (o segundo elemento dos membros da matriz de checks ) é chamado. Se True for retornado, o erro correspondente será encontrado. Se nenhum erro for encontrado, a função mutate será chamada.


Vou explicar:


  • As funções de verificação usam os mesmos argumentos que as funções de mutate .
  • As funções de validação devem retornar True se um erro for encontrado.
  • As verificações de autorização e a presença do elemento raiz são bastante gerais e estão listadas nos sinalizadores Meta .
  • authentication_required adiciona verificação de autorização se True .
  • root_required adiciona uma verificação " root is not None ".
  • UpdatePostMutationErrors não UpdatePostMutationErrors mais necessário. Uma união de possíveis erros é criada rapidamente, dependendo das classes de erros da matriz de checks .



Genéricos


DefaultMutation usado na última seção adiciona um método pre_mutate , que permite alterar os argumentos de entrada antes de verificar se há erros e, consequentemente, invocar a mutação.


Há também um kit inicial genérico que reduz o código e facilita a vida.
Nota: atualmente o código genérico é específico ao django ORM


Createmutation


Requer um dos create_function model ou função de create_function . Por padrão, create_function fica assim:


 model._default_manager.create(**data, owner=user) 

Isso pode parecer inseguro, mas não esqueça que há verificação de tipo embutida no graphql, além de verificação de mutações.


Ele também fornece um método post_mutate chamado após a função create_function com argumentos (instance_created, user) , cujo resultado será retornado ao cliente.


Updatemutation


Permite definir a função update_function . Por padrão:


 def default_update_function(instance, user=None, **data): instance.__dict__.update(data) instance.save() return instance 

root_required é True por padrão.


Ele também fornece um método post_mutate chamado após update_function com argumentos (instance_updated, user) , cujo resultado será retornado ao cliente.


E é disso que precisamos!


Código final:


 class UpdatePostMutation(UpdateMutation): class Arguments: input = UpdatePostInput() class Meta: checks = [ ( PostErrors.title_too_short, lambda post, input: len(input.title) < TITLE_MIN_LENGTH ), ( PostErrors.title_already_taken, lambda post, input: Post.objects.filter(title=input.title).exists() ), ] 

DeleteMutation


Permite definir a função delete_function . Por padrão:


 def default_delete_function(instance, user=None, **data): instance.delete() 



Conclusão


Este artigo considera apenas um aspecto, embora, na minha opinião, seja o mais complexo. Eu tenho alguns pensamentos sobre resolvedores e tipos, bem como coisas gerais em grafeno-python.


É difícil para mim me chamar de desenvolvedor experiente, por isso ficarei muito satisfeito com qualquer feedback e sugestões.


O código fonte pode ser encontrado aqui .

Source: https://habr.com/ru/post/pt461939/


All Articles