Pydantic とは
Pydantic は、Python の型アノテーションを利用して、実行時における型ヒントを提供したり、データのバリデーション時のエラー設定を簡単に提供してくれるためのライブラリです。
このライブラリは、SQLAlchemyでのデータベースモデルを定義する際に役立ちます。
モデル
まず、定義するにあたって、次のように定義します。
from datetime import datetime
from typing import List
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str # (変数):(型)として、型を宣言する
friendIds: List[int] = [] # "=" を利用してデフォルト値を定義することもできます
created_at: datetime
external_data={
'id': '1',
'name' :'太郎さん',
'created_at': '2019-11-03 03:34',
'friendIds': [1,3]
}
user = User(**external_data)
print(user.id)
#> 1 # 入力された値が string 型でも、Int型に自動変換してくれます
print(repr(user.created_at))
#> datetime.datetime(2019, 11, 3, 3, 34)
print(user.friendIds)
#> [1,3]
print(user.dict())
"""
{
'id': 1,
'name': '太郎さん',
'created_at': datetime.datetime(2019, 11, 3, 3, 34),
'friendIds': [1, 3]
}
"""
では、Validation Error が発生すると、次のようになります。
try:
User(created_at="foo",friendIds=[1,'2','bar'])
except ValidationError as e:
print(e.json())
"""
[
{
"loc": [
"id"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"name"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"created_at"
],
"msg": "invalid datetime format",
"type": "type_error.datetime"
},
{
"loc": [
"friendIds",
2
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
"""
ここで、エラーが発生した際に返されるオブジェクトについては、次の通りです。
- loc
- どこでエラーが発生しているかをリスト型で伝えてくれます。
- 先頭の要素はエラー箇所を、その次の要素は、ネスト状になったエラー箇所の場所を示してくれます。
- type
- エラーの種類を示します
- msg
- エラー理由を説明してくれます。
- ctx:
- 自身で設定した任意のオブジェクトを返す
- この機能を利用するために、エラーメッセージを返すように設定する
SQLAlchemy と組み合わせてみる
では、これをPythonの ORM Wrapper であるSQLAlchemy を利用して、次のように
SQLモデルを設計することができます。
以下のコードは、こちらよりを引用しています。
from typing import List
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr
Base = declarative_base()
class CompanyOrm(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True, nullable=False)
public_key = Column(String(20), index=True, nullable=False, unique=True)
name = Column(String(63), unique=True)
domains = Column(ARRAY(String(255)))
class CompanyModel(BaseModel):
id: int
public_key: constr(max_length=20)
name: constr(max_length=63)
domains: List[constr(max_length=255)]
class Config:
orm_mode = True
co_orm = CompanyOrm(
id=123,
public_key='foobar',
name='Testing',
domains=['example.com', 'foobar.com']
)
print(co_orm)
#> <orm_mode.CompanyOrm object at 0x7f0de1bc1cd0>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']
Validators
Validator デコレーター を利用すれば、入力された値が適切かどうか、オブジェクト間の複雑な関連性を参照できるようにすることができます。
from pydantic import BaseModel, ValidationError, validator
class UserModel(BaseModel):
name: str
username: str
password1: str
password2: str
@validator('name')
def name_must_contain_space(cls, v):
if ' ' not in v:
raise ValueError('must contain a space')
return v.title()
@validator('password2')
def passwords_match(cls, v, values, **kwargs):
if 'password1' in values and v != values['password1']:
raise ValueError('passwords do not match')
return v
@validator('username')
def username_alphanumeric(cls, v):
assert v.isalpha(), 'must be alphanumeric'
return v
print(UserModel(name='samuel colvin', username='scolvin', password1='zxcvbn',
password2='zxcvbn'))
#> name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
UserModel(name='samuel', username='scolvin', password1='zxcvbn',
password2='zxcvbn2')
except ValidationError as e:
print(e)
"""
2 validation errors for UserModel
name
must contain a space (type=value_error)
password2
passwords do not match (type=value_error)
"""
Pre and per-item validators
Validator を起動させる際の優先順を設定するには、次の引数pre
,pre_item
を利用します。
pre
は、設定したほかのValidator よりも先にValidatorを起動します。
each_item=True
とすると、リストや辞書、といった各要素ごとにValidation を実行してくれます。
from typing import List
from pydantic import BaseModel, ValidationError, validator
class DemoModel(BaseModel):
square_numbers: List[int] = []
cube_numbers: List[int] = []
# '*' is the same as 'cube_numbers', 'square_numbers' here:
@validator('*', pre=True)
def split_str(cls, v):
if isinstance(v, str):
return v.split('|')
return v
@validator('cube_numbers', 'square_numbers')
def check_sum(cls, v):
if sum(v) > 42:
raise ValueError(f'sum of numbers greater than 42')
return v
@validator('square_numbers', each_item=True)
def check_squares(cls, v):
assert v ** 0.5 % 1 == 0, f'{v} is not a square number'
return v
@validator('cube_numbers', each_item=True)
def check_cubes(cls, v):
# 64 ** (1 / 3) == 3.9999999999999996 (!)
# this is not a good way of checking cubes
assert v ** (1 / 3) % 1 == 0, f'{v} is not a cubed number'
return v
print(DemoModel(square_numbers=[1, 4, 9]))
#> square_numbers=[1, 4, 9] cube_numbers=[]
print(DemoModel(square_numbers='1|4|16'))
#> square_numbers=[1, 4, 16] cube_numbers=[]
print(DemoModel(square_numbers=[16], cube_numbers=[8, 27]))
#> square_numbers=[16] cube_numbers=[8, 27]
try:
DemoModel(square_numbers=[1, 4, 2])
except ValidationError as e:
print(e)
"""
1 validation error for DemoModel
square_numbers -> 2
2 is not a square number (type=assertion_error)
"""
try:
DemoModel(cube_numbers=[27, 27])
except ValidationError as e:
print(e)
"""
1 validation error for DemoModel
cube_numbers
sum of numbers greater than 42 (type=value_error)
"""
Validate Always
パフォーマンスの理由から、デフォルトで、値が与えられていない場合ではこのValidatorは起動しません。しかし、値が与えられていない場合でも実行させるためには、引数をalways= True
と設定する必要があります。
from datetime import datetime
from pydantic import BaseModel, validator
class DemoModel(BaseModel):
ts: datetime = None
@validator('ts', pre=True, always=True)
def set_ts_now(cls, v):
return v or datetime.now()
print(DemoModel())
#> ts=datetime.datetime(2019, 10, 24, 15, 7, 51, 449261)
print(DemoModel(ts='2017-11-08T14:00'))
#> ts=datetime.datetime(2017, 11, 8, 14, 0)
最後に
以上が、基本的なPydantic の使い方です。これを利用して、 SQLのスチーマにバリデーションや型をPythonで設定できます。
詳細な設定については、以下の公式ドキュメントを参照ください。