Schematics Python: Visão Geral e Tutorial
by Graciele Damasceno
O que é schematics?
Schematics é uma lib que auxilia no design, conversão e validação de estruturas de dados em python. Essa biblioteca elimina a necessidade de criar e manipular diretamente dados de payload para o contexto da aplicação.
Para começar, usamos um pip install assim:
pip install schematics
Neste exemplo, vamos ingerir um esquema de dados complexo usando Schematics. Os dados chegam em um formato json, conforme exemplificado abaixo:
{
"_id": "641505586f0763093fe5de82",
"index": 0,
"guid": "3e591c0d-5812-4c1e-9062-91d67db36326",
"isActive": True,
"balance": "$3,657.06",
"picture": "https://picsum.photos/200",
"age": 22,
"eyeColor": "brown",
"name": "Whitehead Navarro",
"gender": "male",
"company": "ROUGHIES",
"email": "whiteheadnavarro@roughies.com",
"phone": "(843) 429-2875",
"address": "934 River Street, Sparkill, New Mexico, 4866",
"about": "Mollit enim aute sint enim ut eiusmod dolore dolore veniam. Esse ad consequat pariatur "
"cupidatat qui deserunt proident minim irure. Proident labore minim ex voluptate ea ut excepteur "
"duis ad minim quis incididunt labore. Laborum sit aliqua aliqua et ad qui qui quis ullamco. "
"Voluptate voluptate consectetur nostrud amet enim. In Lorem voluptate fugiat duis. Ut proident "
"ipsum minim do fugiat sunt laboris voluptate tempor aliqua aliquip deserunt sit.\r\n",
"registered": "2018-08-26T04:14:49",
"coordinates": [
-0.642558,
-154.849655
],
"tags": [
"pariatur",
"consequat",
"et",
"amet",
"fugiat",
"non",
"deserunt"
],
"friends": [
{
"id": 0,
"name": "Alejandra Kinney"
},
{
"id": 1,
"name": "Holmes Graves"
},
{
"id": 2,
"name": "Barbra Dominguez"
}
],
"greeting": "Hello, Whitehead Navarro! You have 7 unread messages.",
"favoriteFruit": "strawberry",
"favoriteMedia": {
"name": "Better Call Saul",
"year": "2022",
"network": "AMC"
}
}
1. Recebendo e Convertendo Dados
Para converter os dados usando schematics, a primeira coisa a fazer é criar um modelo que represente uma pessoa:
import datetime
from schematics import Model
from schematics.transforms import blacklist, whitelist
from schematics.types import ModelType, StringType, IntType, UUIDType, BooleanType, URLType, EmailType, DateTimeType, \
GeoPointType, ListType, serializable, PolyModelType
from model.friend import Friend
from model.game import Game
from model.movie import Movie
from model.schematics_types.currency_type import CurrencyType
from model.tv_show import TVShow
from model.validators import is_uppercase, is_email_valid, is_over_18
class Person(Model):
id = StringType(deserialize_from='_id')
index = IntType()
guid = UUIDType()
is_active = BooleanType(deserialize_from='isActive')
balance = CurrencyType()
picture = URLType()
age = IntType(validators=[is_over_18])
eye_color = StringType(deserialize_from='eyeColor')
name = StringType(required=True)
gender = StringType()
company = StringType(validators=[is_uppercase])
email = EmailType(validators=[is_email_valid], required=True)
phone = StringType(required=True)
address = StringType()
about = StringType()
registered = DateTimeType()
coordinates = GeoPointType()
tags = ListType(StringType)
friends = ListType(ModelType(Friend))
greeting = StringType()
favorite_fruit = StringType(deserialize_from='favoriteFruit')
favorite_media = PolyModelType([
Movie,
TVShow,
Game
], deserialize_from='favoriteMedia')
created_at = DateTimeType(default=datetime.datetime.now)
@serializable
def external_id(self):
return u'%s-%s' % (self.index, self.id)
class Options:
serialize_when_none = False
roles = {
'public_person': blacklist('id', 'index', 'guid', 'is_active', 'balance'),
'profile_info': whitelist('name', 'greeting', 'gender', 'picture', 'about', 'age')
}
Muita informação aqui! Vamos entender passo a passo.
Atribuindo tipos a campos
Criamos uma classe Person
que estende a classe Model
do Schematics. Nesta classe, todas as variáveis são declaradas com um tipo específico já existente no Schematics ou um tipo customizado criados por nós mesmos. O Schematics fornece tipos prontos para uso (como StringType
, UUIDType
, IntType
; verifique todos os tipos disponíveis usando o as documentações do Schematics).
Esses tipos são muito úteis para coagir e converter dados para corresponder ao nosso esquema de classe desejado. Além disso, com um tipo, a validação de campo se torna muito fácil (falaremos mais sobre isso já já).
Como visto acima, é possível criar tipos personalizados para atender às nossas necessidades específicas, como a classe CurrencyType
:
from schematics.types import FloatType
class CurrencyType(FloatType):
def convert(self, value, context=None):
if not isinstance(value, str):
return value
number = value.replace('$', '')
return float(number.replace(',', ''))
Neste exemplo, CurrencyType
estende a classe FloatType
do Schematics, o método de conversão recebe um valor - e se tratando de uma string, remove o cifrão e a vírgula para aderir ao formato de moeda desejado.
Lista de Modelos
Para representar uma lista/array de itens de um modelo, usamos ListType(ModelType())
e o nome do modelo que compõem essa lista - como no exemplo acima: ListType(ModelType(Friend))
. A classe Friend
representa um modelo da seguinte forma:
from schematics import Model
from schematics.transforms import wholelist
from schematics.types import IntType, StringType
class Friend(Model):
id = IntType()
name = StringType()
Se a lista consistir em um tipo simples, como o parâmetro tags
(uma lista composta apenas de strings), declaramos um ListType()
com o tipo desejado, como ListType(StringType)
.
Renomeando Campos
Schematics faz atribuições automáticas para atribuir as variáveis baseadas no nome do campo recebido com o campo do nosso modelo; quando ambos compartilham o mesmo nome, esse processo acontece por baixo dos panos. Mas o que acontece quando recebemos campos que em nosso contexto possuem nomes diferentes?
Para resolver isso, é bastante fácil: basta adicionar deserialize_from=
seguido do nome do campo original recebido para realizar a correspondência. No exemplo acima, os campos id
, eye_color
e favorite_fruit
são desserializados usando este atributo:
favorite_fruit = StringType(deserialize_from='favoriteFruit')
Esta é uma ótima maneira de normalizar facilmente payloads com nomenclatura diferente do snake_case, que é usado pelo python.
Campos Padrão
Para definir um valor padrão para um campo, use o atributo default=
dentro do tipo de campo desejado. No exemplo acima, o campo created_at
terá como padrão a data/hora em que o modelo foi instanciado.
created_at = DateTimeType(default=datetime.datetime.now)
Campos Compostos
Para criar um campo composto de campos existentes na classe, crie uma função com o nome do campo desejado anotado com @serializable
como no campo external_id
do model acima, que é criado concatenando os campos index
e id
. Isso permite que o valor seja acessado como qualquer outro campo da classe.
@serializable
def external_id(self):
return u'%s-%s' % (self.index, self.id)
Definindo validadores Personalizados
Podemos criar um validador personalizado para verificar se os dados recebidos respeitam regras de negócio. Para isso, crie uma função com a validação desejada que receba um atributo value
como parâmetro e gere uma exceção ValidationError
em um cenário negativo, como este:
import re
from schematics.exceptions import ValidationError
def is_uppercase(value):
if value.upper() != value:
raise ValidationError('Field should be uppercase.')
return value
def is_email_valid(value):
regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b'
if not re.fullmatch(regex, value):
raise ValidationError('E-mail address invalid.')
return value
def is_over_18(value):
if value < 18:
raise ValidationError('User cannot be underage.')
return value
Para validar os elementos do modelo, use validators=[]
e o nome da função entre colchetes, assim: validators=[is_email_valid]
.
email = EmailType(validators=[is_email_valid], required=True)
Campos Obrigatórios
Para definir um campo como obrigatório, use required=True
na declaração do tipo. Se o valor não estiver presente no momento da validação, a aplicação lançará uma exceção.
2. Acessando e Manipulando Dados
Com nosso modelo criado, é hora de gerar alguns dados carregando o json recebido como um dict e passando como parâmetro ao instanciar classe Person
:
person = Person(json_input)
Validando Dados
Para validar se os dados recebidos estão em corretos, usamos o seguinte método:
person.validate()
Em nosso modelo, criamos um validador que especifica que o valor do campo empresa deve ser maiúsculo. Deixar de informar uma string em letras maiúsculas resulta na seguinte saída quando validado:
schematics.exceptions.DataError: {"company": ["Field should be uppercase."]}
Process finished with exit code 1
Ignorando Campos Não Mapeados
Para ignorar campos não declarados no modelo e presentes na entrada, usamos o seguinte atributo de validação:
person.validate(strict=False)
Se este parâmetro não for especificado, passar um campo na entrada que não está contido no modelo resulta na seguinte saída ao validar:
schematics.exceptions.DataError: {"newField": "Rogue field"}
Process finished with exit code 1
Omitindo Campos Sem Valor
Para omitir campos sem valor (atribuídos com None
) ao exportar um modelo, use serialize_when_none = False
dentro da classe interna Options
:
class Options:
serialize_when_none = False
Acessando Campos
Com o objeto modelo criado, fica fácil acessar os campos:
print(person.name, 'is', person.age, 'old.')
que gera como resposta:
Whitehead Navarro is 22 old.
Process finished with exit code 0
Exportando para Json
Para exportar o objeto para json, utilizamos o seguinte método:
json.dumps(person.to_primitive())
que gera como resposta o json:
{"id": "641505586f0763093fe5de82", "index": 0, "guid": "3e591c0d-5812-4c1e-9062-91d67db36326", "is_active": true, "balance": 3657.06, "picture": "https://picsum.photos/200", "age": 22, "eye_color": "brown", "name": "Whitehead Navarro", "gender": "male", "company": "ROUGHIES", "email": "whiteheadnavarro@roughies.com", "phone": "(843) 429-2875", "address": "934 River Street, Sparkill, New Mexico, 4866", "about": "Mollit enim aute sint enim ut eiusmod dolore dolore veniam. Esse ad consequat pariatur cupidatat qui deserunt proident minim irure. Proident labore minim ex voluptate ea ut excepteur duis ad minim quis incididunt labore. Laborum sit aliqua aliqua et ad qui qui quis ullamco. Voluptate voluptate consectetur nostrud amet enim. In Lorem voluptate fugiat duis. Ut proident ipsum minim do fugiat sunt laboris voluptate tempor aliqua aliquip deserunt sit.\r\n", "registered": "2018-08-26T04:14:49.000000", "coordinates": [-0.642558, -154.849655], "tags": ["pariatur", "consequat", "et", "amet", "fugiat", "non", "deserunt"], "friends": [{"id": 0, "name": "Alejandra Kinney"}, {"id": 1, "name": "Holmes Graves"}, {"id": 2, "name": "Barbra Dominguez"}], "greeting": "Hello, Whitehead Navarro! You have 7 unread messages.", "favorite_fruit": "strawberry", "created_at": "2023-06-22T22:09:15.939875", "external_id": "0-641505586f0763093fe5de82"}
Process finished with exit code 0
Roles e Opções de Classe
Uma role funciona como um filtro ao exportar dados usando as opções whitelist
e blacklist
. Como no modelo Person
acima, declaramos uma role dentro da subclasse Option e listamos os valores da whitelist
(valores para exportar) ou uma blacklist
(valores para omitir).
class Options:
roles = {
'public_person': blacklist('id', 'index', 'guid', 'is_active', 'balance'),
'profile_info': whitelist('name', 'greeting', 'gender', 'picture', 'about', 'age')
}
Este é o comando para imprimir apenas os valores da whitelist:
print(person.to_primitive(role='profile_info'))
que imprime os valores de pictures
, age
, name
, gender
, about
e greeting
:
{'picture': 'https://picsum.photos/200', 'age': 22, 'name': 'Whitehead Navarro', 'gender': 'male', 'about': 'Mollit enim aute sint enim ut eiusmod dolore dolore veniam. Esse ad consequat pariatur cupidatat qui deserunt proident minim irure. Proident labore minim ex voluptate ea ut excepteur duis ad minim quis incididunt labore. Laborum sit aliqua aliqua et ad qui qui quis ullamco. Voluptate voluptate consectetur nostrud amet enim. In Lorem voluptate fugiat duis. Ut proident ipsum minim do fugiat sunt laboris voluptate tempor aliqua aliquip deserunt sit.\r\n', 'greeting': 'Hello, Whitehead Navarro! You have 7 unread messages.'}
Process finished with exit code 0
Ao imprimir todos os valores exceto os que estão na blacklist, usamos:
print(person.to_primitive(role='public_person'))
que omite os campos id
, index
, guid
, is_active
e balance
:
{'picture': 'https://picsum.photos/200', 'age': 22, 'eye_color': 'brown', 'name': 'Whitehead Navarro', 'gender': 'male', 'company': 'ROUGHIES', 'email': 'whiteheadnavarro@roughies.com', 'phone': '(843) 429-2875', 'address': '934 River Street, Sparkill, New Mexico, 4866', 'about': 'Mollit enim aute sint enim ut eiusmod dolore dolore veniam. Esse ad consequat pariatur cupidatat qui deserunt proident minim irure. Proident labore minim ex voluptate ea ut excepteur duis ad minim quis incididunt labore. Laborum sit aliqua aliqua et ad qui qui quis ullamco. Voluptate voluptate consectetur nostrud amet enim. In Lorem voluptate fugiat duis. Ut proident ipsum minim do fugiat sunt laboris voluptate tempor aliqua aliquip deserunt sit.\r\n', 'registered': '2018-08-26T04:14:49.000000', 'coordinates': [-0.642558, -154.849655], 'tags': ['pariatur', 'consequat', 'et', 'amet', 'fugiat', 'non', 'deserunt'], 'friends': [{'id': 0, 'name': 'Alejandra Kinney'}, {'id': 1, 'name': 'Holmes Graves'}, {'id': 2, 'name': 'Barbra Dominguez'}], 'greeting': 'Hello, Whitehead Navarro! You have 7 unread messages.', 'favorite_fruit': 'strawberry', 'created_at': '2023-06-22T22:17:12.004692', 'external_id': '0-641505586f0763093fe5de82'}
Process finished with exit code 0
Mockando a Resposta
Para testar nosso código, podemos usar Schematics para mockar os valores do modelo:
print(Person.get_mock_object().to_primitive())
O mock tem valores aleatórios baseados nos tipos das variáveis:
{'index': 14, 'picture': 'http://aiV9Q.ZZ', 'age': 12, 'eye_color': 'N', 'name': 'hgqn8YPvDqLbf', 'company': 'JQ', 'email': 'ER@example.com', 'phone': 'DQVbv9q', 'address': '61sOKCSGGOkV', 'coordinates': (-83, 53), 'greeting': 'fsxF', 'created_at': '2127-03-09T03:39:51.357227+1130', 'external_id': '14-None'}
Process finished with exit code 0
3. Modelagem Avançada: Polimorfismo
Também é possível realizar polimorfismo de tipos usando Schematics! Quando temos um campo que pode ter vários tipos, podemos declará-lo assim:
favorite_media = PolyModelType([
Movie,
TVShow,
Game
], deserialize_from='favoriteMedia')
As classes Movie
, TVShow
e Game
representam os tipos aceitos do campo favorite_media
:
from schematics import Model
from schematics.types import StringType, IntType
class Movie(Model):
name = StringType()
year = IntType()
from schematics import Model
from schematics.types import StringType, IntType
class TVShow(Model):
name = StringType()
year = IntType()
network = StringType()
@classmethod
def _claim_polymorphic(cls, data):
return data.get('network')
from schematics import Model
from schematics.types import StringType, IntType
class Game(Model):
name = StringType()
year = IntType()
console = StringType()
@classmethod
def _claim_polymorphic(cls, data):
return data.get('console')
Nossos modelos são muito parecidos. Neste caso, devemos implementar o método _claim_polymorphic
que ajuda o Schematics a diferenciar entre os modelos com base no payload recebido. Neste caso, retornamos um data.get
com um atributo que seja único entre todas as classes.
Para verificar o tipo de mídia favorito escolhido pelos Schematics após a ingestão dos dados recebidos, usamos:
print(person.favorite_media)
que gera como resposta:
<TVShow instance>
Process finished with exit code 0
A saída é definida como uma instância de TvShow
porque informamos um atributo network
em nosso esquema recebido.
Para acessar o nome da mídia, podemos acessar diretamente o campo de nome:
print(person.favorite_media.name)
que gera como resposta:
Better Call Saul
Process finished with exit code 0
Se alterarmos a entrada para representar um filme, por exemplo, com uma entrada como esta:
"favoriteMedia": {
"name": "Everything Everywhere All At Once",
"year": "2022"
}
Ao enviar o tipo de mídia favorito, vamos ter:
<Movie instance>
Process finished with exit code 0
É isso! Para acessar esta implementação, confira o código-fonte no meu Github.
Referências
Subscribe via RSS