Eu sou o principal mantenedor de uma significativa API REST escrita usando Flask-Restful. Flash-Restful é uma biblioteca linda, cheia de recursos para a criação de frameworks Rest com Flask. Ela tem tudo o que você precisa para gerenciar o roteamento de recursos baseados em classe, uma biblioteca para mobilizar objetos para o formato JSON, e reqparse, uma biblioteca inspirada em argparse para escrever input parsers.
É sobre reqparse que irei falar hoje.
Uma introdução ao Reqparse
Reqparse tem uma UI muito simples:
from flask.ext.restful import reqparse parser = reqparse.RequestParser() parser.add_argument( "name", type=str, required=True ) # Within a flask request context args = parser.parse_args() args.get('name') # => The "name" parameter from the request
Você declara o parser e, em seguida, adiciona argumentos a ele. Cada argumento é, na verdade, uma instância de uma classe Argument, que recebe os argumentos passados para add_argument. Em termos gerais, aqui está um pseudocódigo explicando como reqparse funciona:
class Argument(object): def __init__(self, name, **kwargs): self.name = name self.type = kwargs.pop('type') def parse(self, data): val = data.get(self.name) try: return self.type(val) except Exception as e: return ValueError(e.message) class RequestParser(object): def __init__(self): self.args = [] def add_argument(self, name, **kwargs): self.args.append(Argument(name, **kwargs)) def parse_args(self): results = {} for arg in self.args: result = arg.parse(flask.request.json) if isinstance(result, ValueError): abort(400, result.message) results[arg.name] = results return results
Isso é tudo muito lindo, mas se você já escreveu qualquer tipo de código de validação antes, deve ter notado todos os tipos de casos que dificultam o que foi mostrado acima. Vou focar em: o argumento de palavra-chave é necessário?
Um conto de duas filosofias
O código acima é um pseudocódigo, mas o cerne da questão aqui é: qual a melhor forma de implementar o que foi mostrado acima?
Para contar a história fora de ordem, a abordagem do reqparse (como eu aprendi quando eu tentei atualizá-lo) envolve a adição de mais kwargs a Argument e RequestParser, e atualizar suas implementações com casos especiais para esse argumento. Como consequência, é assim que o método parse do Argument se parece agora:
def parse(self, request, bundle_errors=False): """Parses argument value(s) from the request, converting according to the argument's type. :param request: The flask request object to parse arguments from :param do not abort when first error occurs, return a dict with the name of the argument and the error message to be bundled """ source = self.source(request) results = [] # Sentinels _not_found = False _found = True for operator in self.operators: name = self.name + operator.replace("=", "", 1) if name in source: # Account for MultiDict and regular dict if hasattr(source, "getlist"): values = source.getlist(name) else: values = [source.get(name)] for value in values: if hasattr(value, "strip") and self.trim: value = value.strip() if hasattr(value, "lower") and not self.case_sensitive: value = value.lower() if hasattr(self.choices, "__iter__"): self.choices = [choice.lower() for choice in self.choices] try: value = self.convert(value, operator) except Exception as error: if self.ignore: continue return self.handle_validation_error(error, bundle_errors) if self.choices and value not in self.choices: if current_app.config.get("BUNDLE_ERRORS", False) or bundle_errors: return self.handle_validation_error( ValueError(u"{0} is not a valid choice".format( value)), bundle_errors) self.handle_validation_error( ValueError(u"{0} is not a valid choice".format( value)), bundle_errors) if name in request.unparsed_arguments: request.unparsed_arguments.pop(name) results.append(value) if not results and self.required: if isinstance(self.location, six.string_types): error_msg = u"Missing required parameter in {0}".format( _friendly_location.get(self.location, self.location) ) else: friendly_locations = [_friendly_location.get(loc, loc) for loc in self.location] error_msg = u"Missing required parameter in {0}".format( ' or '.join(friendly_locations) ) if current_app.config.get("BUNDLE_ERRORS", False) or bundle_errors: return self.handle_validation_error(ValueError(error_msg), bundle_errors) self.handle_validation_error(ValueError(error_msg), bundle_errors) if not results: if callable(self.default): return self.default(), _not_found else: return self.default, _not_found if self.action == 'append': return results, _found if self.action == 'store' or len(results) == 1: return results[0], _found return results, _found
Vou fazer uma pausa aqui para salientar que esse código funciona perfeitamente bem, e Flask-Restful tem sido uma ótima biblioteca para usar. Mas procure por alguma lógica de branch necessária para os casos especiais. Por exemplo, esse ramo deixa o valor lowercase e, em seguida, tem de verificar no argumento choice (usado para especificar um conjunto de valores válidos que a entrada poderia tomar).
if hasattr(value, "lower") and not self.case_sensitive: value = value.lower() if hasattr(self.choices, "__iter__"): self.choices = [choice.lower() for choice in self.choices]
Isso tem cheiro de um bug nascido de um caso extremamente obscuro e, na verdade, podemos encontrar o commit onde esse branch foi adicionado.
Voltando no tempo antes de tudo isso: como nosso aplicativo cresceu mais e mais quanto às exigências dos seus dados, uma biblioteca reutilizável “validators” nasceu. Ela simplesmente fornece funções que podem ser passadas para o parâmetro type do add_argument do reqparse. Ele começou simples o suficiente, com validators como e-mail:
import re EMAIL_RE = re.compile(r".+@.+\..+") def email(s): if not EMAIL_RE.match(s): raise ValidationError("Invalid email format") return s
Eventualmente, porém, ele ganha algumas faixas adicionais. Aqui está o validator choice que foi adicionado em algum momento, provavelmente por mim quando eu não tive paciência para encontrar a opção choice embutida do reqparse.
def choice(choices, msg='Invalid Choice'): def choose(choice): if choice not in choices: raise ValidationError(msg=msg) if isinstance(choices, dict): return choices.get(choice) else: return choice return choose # Usage parser.add_argument( 'category', type=validators.choice(['News', 'Entertainment', 'Other']), )
Viu como a responsabilidade por essa validação é agora transferida para a função type, garantindo que nenhum tratamento especial de código de escolha seja mantido longe das funções de análise de uso geral? Sem querer, a biblioteca validators acrescentou um monte de funcionalidades embutidas do reqparse em paralelo. Em paralelo às palavras-chave do reqparse, validators ganhou optional, required, nullable e validated_list.
Recentemente, eu tive que atualizar Flask-Restful por algum motivo, e foi aterrorizante quando alguns dos validators pararam de funcionar. Isso não foi realmente culpa de ninguém, apenas um caso em que minhas funções de validação esperavam algum comportamento edge-case, que tinha mudado. Mas eu percebi isso, e agora não preciso de todas as funcionalidades que o reqparse oferece. Eu troquei as classes RequestParser e Argument por algumas personalizadas que se parecem muito mais com o exemplo de pseudocódigo acima do que com as implementações atuais que existem dentro do flash-restful. Não é um processo em que tudo é feito de uma vez, mas ao longo do tempo, e todos os validators baseados em palavras-chave estão se movendo para baseados em funções, e implementações Argument e RequestParser continuam ficando mais e mais simples.
Aliás, esse é o tipo de sistema que WTForms, outra ótima biblioteca de imput-wrangling, sempre usou. Cada classe Field aceita uma lista de validators, que podem ser totalmente escritos de forma personalizada, assim como o meu acima (você pode até mesmo torná-los closures retornando funções). O que WTForms fornece, em vez de argumentos extras, é um conjunto de validators que cobrem casos comuns, como DataRequired, EqualTo etc:
class MyForm(Form): email = StringField("email", validators=[DataRequired(), Email()])
Mas, para não ficar atrás no campo da austeridade em palavra-chave, validators há muito tem proporcionado um método comp para esse tipo de situação:
parser.add_argument( 'email', type=validators.comp(validators.required, validators.email) )
Então, qual é a lição? Eu não sei, eu não sou um professor, mas eu acho que existe algo ao longo das linhas: sempre que possível, escolha a composição da função para aqueles casos em que ela realmente torna as coisas mais simples.
***
Adam Bard faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://adambard.com/blog/composible-architecture-a-real-world-example