Por: @mariliars
Publicado em: 2018-11-14
Estrutura das Entidades e Validadores
Com a implementação da estrutura clean arquiteture a equipe de desenvolvimento observou repetição de código para a declaração das entidades e seus validadores. Além disso, também existem problemas com a estrutura de validação. Entre esses problemas estão:
- Validação de estruturas de objetos embutidos, ou seja, um objeto com relacionamento entre outros objetos
- Existe a dificuldade na validação de valores nulos ou de strings vazias
- Não existe a conversão automática dos dados
Diante desses problemas, foi elaborado um estudo sobre formas de estruturar a declaração das entidades e como validar seus campos. Os critérios a serem observados neste estudo são:
- Dados em formato de classes/structs
- Validação embutida nos dados
- Conversão automática de tipos
- Conversão para formato json
- Valores padrões para os campos
As bibliotecas estudadas são listadas a seguir e são detalhadas ao longo deste post.
Além dessas bibliotecas, em pesquisas relacionadas foram encontradas as bibliotecas de validação de dados: Schema e Kim. Para estas, não foram elaborados protótipos. Mais informações no site oficial.
Os exemplos de implementação das bibliotecas aqui mencionadas estão disponíveis no repositório python-fields-validators
.
Attr
A biblioteca attr permite escrever classes com definição de tipos de dados, validação e transformação de dados. Algumas das características da biblioteca são:
- Permite tipagem dos campos
- Permite conversão de tipo dos campos
- Permite adicionar validadores customizados
- Consegue validar dados de subobjetos quando adicionado um validador customizado
- Define valores default
Exemplo de uso
from attr import attrs, Factory, ib, asdict, filters, fields, validators
from typing import List
def validate_str(instance, attribute, value):
if type(value) != str:
raise ValueError("Must be a string")
def validate_list_int(instance, attribute, value):
for item in value:
if type(item) != int:
raise ValueError("Must be a list of integer")
def validate_list_roman_numbers(instance, attribute, value):
if type(value) != list:
raise ValueError("Must be a list of RomanNumber")
for item in value:
if type(item) != RomanNumber:
raise ValueError("Must be a list of RomanNumber")
@attrs
class RomanNumber:
number = ib(type=str, validator=validators.instance_of(str))
@attrs
class AlphabetNumbers(object):
a = ib(type=int, default=42)
b = ib(default=Factory(list))
c = ib(factory=list)
d = ib()
e = ib(factory=dict)
f = ib(default=42,convert=int)
g = ib(default=42, init=False)
h = ib(factory=str, validator=validate_str)
i = ib(factory=int, validator=validators.instance_of(int))
list_number: List[int] = ib(default=Factory(List[int]), validator=validators.and_(validators.instance_of(List), validate_list_int))
list_roman_number: List[RomanNumber] = ib(default=Factory(List[RomanNumber]), validator=validators.and_(validators.instance_of(List), validate_list_roman_numbers))
roman_number: RomanNumber = ib(default=None, validator=validators.instance_of(RomanNumber))
secret_key = ib(factory=str, repr=False)
def __attrs_post_init__(self):
self.g = 50
@d.default
def _factory_d(self):
return {}
def to_dict(self):
return asdict(self)
Schematics
Schematics permite escrever classes com definição de tipos de dados, validação e transformação de dados. Características da biblioteca:
- É possível instanciar o objeto vazio ou com um dicionário
- Campos string aceitam int
- Campos int aceitam números escritos utilizando string
- O valor default dos campos é sempre nulo
- Permite personalizar mensagens
- Permite definir nomes de serialização e deserealização
- Permite adicionar validadores customizados
- Gera mock dos objetos
- Suporte a idiomas
- Permite converter os dados
FormEncode
FormEncode é um pacote de validação e geração de formulários. Algumas de suas características são:
-
Validação dos campos
-
Permite adicionar validadores customizados
-
Cosegue validar regex
-
Validadores nativos:
-
if_missing
: para setar um valor default -
not_empty
: torna o campo requerido -
strip
: para campos do tipo string -
Outros:
if_empty
,if_invalid
,if_invalid_python
,accept_python
Obs: Pouco documentado
Pydantic
Pydantic é uma bibliocteca de validação de dados utilizando o pytdantic BaseModel ou dataclass. Algumas de suas características são:
- As validações são escritas como funções da classe utilizando o decorator
@validator
, informando quais campos irão aplicar a validação. - Possui formas de priorizar as validações, executando uma validação antes da outra utilizando o
pre
- Possui validação de objetos com o
whole
- Permite criar schemas
- Permite criar tipos de dados customizados
- Possui estrutura de mensagens de erro
- Caso não seja definida uma estrutura de validação converte números escritos no formato strnig para inteiro e vice versa
Exemplo de uso
import json
from typing import List
from pydantic import BaseModel, ValidationError, validator
class StrictStr(str):
@classmethod
def get_validators(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if not isinstance(v, str):
raise ValueError(f'strict string: str expected not {type(v)}')
return v
class OtherDemo(BaseModel):
field1 : str = None
field2 : int = None
field3: List[int] = []
class DemoModel(BaseModel):
numbers: List[int] = []
people: List[str] = []
other_demo: OtherDemo = None
list_demo: List[OtherDemo] = []
field_str_custom: StrictStr = None
@validator('numbers', 'people', 'list_demo', whole=True)
def check_list(cls, v):
if not isinstance(v, list):
raise ValueError('Must be int')
return v
@validator('numbers', pre=True)
def check_int(cls, v):
if not isinstance(v, int):
raise ValueError('Must be int')
return v
@validator('people', pre=True)
def check_str(cls, v):
if not isinstance(v, str):
raise ValueError('Must be str')
return v
@validator('list_demo', 'other_demo', pre=True)
def check_demo(cls, v):
if not isinstance(v, OtherDemo):
raise ValueError('Must be OtherDemo')
return v
@validator('numbers')
def check_numbers_low(cls, v):
if v > 4:
raise ValueError(f'number too large {v} > 4')
return v
@validator('numbers', whole=True)
def check_sum_numbers_low(cls, v):
if sum(v) > 8:
raise ValueError(f'sum of numbers greater than 8')
return v
Colander
Colander é uma biblioteca de serialização e desserialização de dados. Algumas de suas características são:
- Definição de schemas de dados em forma de classes
- Permite criar tipos de dados customizados, bem como suas validações
- Consegue validar objetos aninhados
- Converte números escritos no formato string para inteiro e vice versa
Exemplo de uso
from colander import TupleSchema, SchemaNode, Int, String, Range, OneOf, MappingSchema, SequenceSchema, Invalid
class RangedInt(SchemaNode):
schema_type = Int
default = 10
title = 'Ranged Int'
def validator(self, node, cstruct):
if not 0 < cstruct < 10:
raise Invalid(node, 'Must be between 0 and 10')
class Friend(TupleSchema):
rank = SchemaNode(Int(), validator=Range(0, 10))
name = SchemaNode(String())
class Phone(MappingSchema):
location = SchemaNode(String(), validator=OneOf(['home', 'work']))
number = SchemaNode(String())
class Friends(SequenceSchema):
friend = Friend()
class Phones(SequenceSchema):
phone = Phone()
class Person(MappingSchema):
name = SchemaNode(String())
age = RangedInt()
friends = Friends()
phones = Phones(missing=None)