검증기
!!! 경고 "🚧 작업 진행 중" 이 페이지는 진행 중인 작업입니다.
이 페이지는 Pydantic에서 더 복잡한 사용자 정의 유효성 검사기를 생성하기 위한 예제 스니펫을 제공합니다.
Annotated
메타데이터와 함께 사용자 정의 유효성 검사기 사용¶
이 예에서는 Annotated
유형에 연결된 사용자 정의 유효성 검사기를 구성하여 datetime
객체가 지정된 시간대 제약 조건을 준수하는지 확인합니다.
사용자 정의 유효성 검사기는 시간대의 문자열 사양을 지원하며 datetime
객체에 올바른 시간대가 없으면 오류가 발생합니다.
유효성 검사기에서 __get_pydantic_core_schema__
사용하여 주석이 달린 유형(이 경우 datetime
)의 스키마를 사용자 정의합니다. 이를 통해 사용자 정의 유효성 검사 논리를 추가할 수 있습니다. 특히, 우리는 datetime
의 기본 pydantic
검증 전후에 작업을 수행할 수 있도록 wrap
유효성 검사기 기능을 사용합니다.
import datetime as dt
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Callable, Optional
import pytz
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated
from pydantic import (
GetCoreSchemaHandler,
PydanticUserError,
TypeAdapter,
ValidationError,
)
@dataclass(frozen=True)
class MyDatetimeValidator:
tz_constraint: Optional[str] = None
def tz_constraint_validator(
self,
value: dt.datetime,
handler: Callable, # (1)!
):
"""Validate tz_constraint and tz_info."""
# handle naive datetimes
if self.tz_constraint is None:
assert (
value.tzinfo is None
), 'tz_constraint is None, but provided value is tz-aware.'
return handler(value)
# validate tz_constraint and tz-aware tzinfo
if self.tz_constraint not in pytz.all_timezones:
raise PydanticUserError(
f'Invalid tz_constraint: {self.tz_constraint}',
code='unevaluable-type-annotation',
)
result = handler(value) # (2)!
assert self.tz_constraint == str(
result.tzinfo
), f'Invalid tzinfo: {str(result.tzinfo)}, expected: {self.tz_constraint}'
return result
def __get_pydantic_core_schema__(
self,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
return core_schema.no_info_wrap_validator_function(
self.tz_constraint_validator,
handler(source_type),
)
LA = 'America/Los_Angeles'
ta = TypeAdapter(Annotated[dt.datetime, MyDatetimeValidator(LA)])
print(
ta.validate_python(dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LA)))
)
#> 2023-01-01 00:00:00-07:53
LONDON = 'Europe/London'
try:
ta.validate_python(
dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LONDON))
)
except ValidationError as ve:
pprint(ve.errors(), width=100)
"""
[{'ctx': {'error': AssertionError('Invalid tzinfo: Europe/London, expected: America/Los_Angeles')},
'input': datetime.datetime(2023, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>),
'loc': (),
'msg': 'Assertion failed, Invalid tzinfo: Europe/London, expected: America/Los_Angeles',
'type': 'assertion_error',
'url': 'https://errors.pydantic.dev/2.8/v/assertion_error'}]
"""
handler
함수는 표준pydantic
검증을 통해 입력을 검증하기 위해 호출하는 것입니다.- 이 랩 유효성 검사기에서 표준
pydantic
유효성 검사를 사용하여 입력 유효성을 검사하기 위해handler
함수를 호출합니다.
비슷한 방식으로 UTC 오프셋 제약 조건을 적용할 수도 있습니다. lower_bound
및 upper_bound
가 있다고 가정하면 datetime
에 정의한 경계 내에 포함되는 UTC 오프셋이 있는지 확인하는 사용자 지정 유효성 검사기를 만들 수 있습니다.
import datetime as dt
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Callable
import pytz
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated
from pydantic import GetCoreSchemaHandler, TypeAdapter, ValidationError
@dataclass(frozen=True)
class MyDatetimeValidator:
lower_bound: int
upper_bound: int
def validate_tz_bounds(self, value: dt.datetime, handler: Callable):
"""Validate and test bounds"""
assert value.utcoffset() is not None, 'UTC offset must exist'
assert self.lower_bound <= self.upper_bound, 'Invalid bounds'
result = handler(value)
hours_offset = value.utcoffset().total_seconds() / 3600
assert (
self.lower_bound <= hours_offset <= self.upper_bound
), 'Value out of bounds'
return result
def __get_pydantic_core_schema__(
self,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
return core_schema.no_info_wrap_validator_function(
self.validate_tz_bounds,
handler(source_type),
)
LA = 'America/Los_Angeles' # UTC-7 or UTC-8
ta = TypeAdapter(Annotated[dt.datetime, MyDatetimeValidator(-10, -5)])
print(
ta.validate_python(dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LA)))
)
#> 2023-01-01 00:00:00-07:53
LONDON = 'Europe/London'
try:
print(
ta.validate_python(
dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LONDON))
)
)
except ValidationError as e:
pprint(e.errors(), width=100)
"""
[{'ctx': {'error': AssertionError('Value out of bounds')},
'input': datetime.datetime(2023, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>),
'loc': (),
'msg': 'Assertion failed, Value out of bounds',
'type': 'assertion_error',
'url': 'https://errors.pydantic.dev/2.8/v/assertion_error'}]
"""
중첩 모델 필드 검증¶
여기서는 검증기가 상위 모델의 데이터를 활용하는 중첩 모델의 필드를 검증하는 두 가지 방법을 보여줍니다.
이 예에서는 각 사용자의 비밀번호가 상위 모델에서 지정한 금지된 비밀번호 목록에 없는지 확인하는 유효성 검사기를 구성합니다.
이를 수행하는 한 가지 방법은 외부 모델에 사용자 정의 유효성 검사기를 배치하는 것입니다.
from typing import List
from typing_extensions import Self
from pydantic import BaseModel, ValidationError, model_validator
class User(BaseModel):
username: str
password: str
class Organization(BaseModel):
forbidden_passwords: List[str]
users: List[User]
@model_validator(mode='after')
def validate_user_passwords(self) -> Self:
"""Check that user password is not in forbidden list. Raise a validation error if a forbidden password is encountered."""
for user in self.users:
current_pw = user.password
if current_pw in self.forbidden_passwords:
raise ValueError(
f'Password {current_pw} is forbidden. Please choose another password for user {user.username}.'
)
return self
data = {
'forbidden_passwords': ['123'],
'users': [
{'username': 'Spartacat', 'password': '123'},
{'username': 'Iceburgh', 'password': '87'},
],
}
try:
org = Organization(**data)
except ValidationError as e:
print(e)
"""
1 validation error for Organization
Value error, Password 123 is forbidden. Please choose another password for user Spartacat. [type=value_error, input_value={'forbidden_passwords': [...gh', 'password': '87'}]}, input_type=dict]
"""
또는 중첩된 모델 클래스( User
)에서 사용자 지정 유효성 검사기를 사용할 수 있으며, 부모 모델의 금지된 비밀번호 데이터는 유효성 검사 컨텍스트를 통해 전달됩니다.
!!! 경고 유효성 검사기 내에서 컨텍스트를 변경하는 기능은 중첩된 유효성 검사에 많은 기능을 추가하지만 혼란스럽거나 디버그하기 어려운 코드로 이어질 수도 있습니다. 이 접근 방식을 사용하는 데 따른 위험은 사용자 본인의 책임입니다!
from typing import List
from pydantic import BaseModel, ValidationError, ValidationInfo, field_validator
class User(BaseModel):
username: str
password: str
@field_validator('password', mode='after')
@classmethod
def validate_user_passwords(
cls, password: str, info: ValidationInfo
) -> str:
"""Check that user password is not in forbidden list."""
forbidden_passwords = (
info.context.get('forbidden_passwords', []) if info.context else []
)
if password in forbidden_passwords:
raise ValueError(f'Password {password} is forbidden.')
return password
class Organization(BaseModel):
forbidden_passwords: List[str]
users: List[User]
@field_validator('forbidden_passwords', mode='after')
@classmethod
def add_context(cls, v: List[str], info: ValidationInfo) -> List[str]:
if info.context is not None:
info.context.update({'forbidden_passwords': v})
return v
data = {
'forbidden_passwords': ['123'],
'users': [
{'username': 'Spartacat', 'password': '123'},
{'username': 'Iceburgh', 'password': '87'},
],
}
try:
org = Organization.model_validate(data, context={})
except ValidationError as e:
print(e)
"""
1 validation error for Organization
users.0.password
Value error, Password 123 is forbidden. [type=value_error, input_value='123', input_type=str]
"""
context 속성이 model_validate
에 포함되지 않은 경우 info.context
None
되며 금지된 비밀번호 목록은 위 구현에서 컨텍스트에 추가되지 않습니다. 따라서 validate_user_passwords
원하는 비밀번호 유효성 검사를 수행하지 않습니다.
유효성 검사 컨텍스트에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
本文总阅读量次