본문 바로가기
Python/Python 개념

Python Schema library 정리

by hyun-am 2022. 12. 19.

schema는 JSON/YAML(또는 다른것들)에서 python 데이터 타입으로 변환된 config file, forms, 외부 서비스 또는 command-line 파싱에서 얻은 것과 같은 python 데이터 구조를 검증하기 위한 라이브러리 입니다.

Example

다음은 개인 정보가 포함된 항목 목록의 유효성을 검사하여 스키마를 파악하는 간단한 예입니다.

from schema import Schema, And, Use, Optional, SchemaError

schema = Schema([{'name': And(str, len),
               'age':  And(Use(int), lambda n: 18 <= n <= 99),
               Optional('gender'): And(str, Use(str.lower),
                                         lambda s: s in ('squid', 'kid'))}])

data = [{'name': 'Sue', 'age': '28', 'gender': 'Squid'},
     {'name': 'Sam', 'age': '42'},
     {'name': 'Sacha', 'age': '20', 'gender': 'KID'}]

validated = schema.validate(data)

assert validated == [{'name': 'Sue', 'age': 28, 'gender': 'squid'},
		   {'name': 'Sam', 'age': 42},
			 {'name': 'Sacha', 'age' : 20, 'gender': 'kid'}]

데이터가 유효한 경우 Schema.validate는 검증된 데이터를 반환합니다.(Optional한 경우 Use를 이용해서 변환)

데이터가 유효하지 않으면 스키마는 SchemaError 예외를 발생시킵니다. 데이터가 유효한지 확인하려는 경우 schema.is_validate(data)는 True or False를 반환

Schema vlidates data 사용

types

Schema(..)가 type(ex. int, str, object 등)을 만나면 해당 데이터 조각이 해당 유형의 인스턴스인지 확인하고, 그렇지 않으면 SchemaError를 발생시킵니다.

from schema import Schema

>>> Schema(int).validate(123)
123

>>> Schema(int).validate('123')
Traceback (most recent call last):
...
schema.SchemaUnexpectedTypeError: '123' should be instance of 'int'

>>> Schema(object).validate('hai')
'hai'

Callables

Schema(…)가 callables(함수, 클래스 또는 _ call _ 메서드를 가진 객체)를 발견하면 호출하고 반환 값이 True로 평가되면 계속 유효성 검사를 수행하며 그렇지 않으면 SchemaError가 발생합니다.

>>> import os

>>> Schema(os.path.exists).validate('./')
'./'

>>> Schema(os.path.exists).validate('./non-existent/')
Traceback (most recent call last):
...
schema.SchemaError: exists('./non-existent/') should evaluate to True

>>> Schema(lambda n: n > 0).validate(123)
123

>>> Schema(lambda n: n > 0).validate(-12)
Traceback (most recent call last):
...
schema.SchemaError: <lambda>(-12) should evaluate to True

Validatables

Schema(…)가 validate 메소드가 있는 object를 만나면 해당 데이터에 대해 이 메서드를 data=obj.validate(data)로 실행합니다. 이 메서드는 SchemaError 예외를 발생시킬 수 있습니다. 이 예외는 Schema에 해당 데이터 조각이 유효하지 않음을 알리고 그렇지 않으면 계속 유효성 검사를 수행합니다.

“유효성"의 예는 문자열이나 버퍼를 주어진 정규 표현식(문자열, 버퍼 또는 컴파일된 정규식 SRE_Pattern)와 일치시키려는 Regex입니다.

>>> from schema import Regex
>>> import re

>>> Regex(r'^foo').validate('foobar')
'foobar'

>>> Regex(r'^[A-Z]+$', flags=re.I).validate('those-dashes-dont-match')
Traceback (most recent call last):
...
schema.SchemaError: Regex('^[A-Z]+$', flags=re.IGNORECASE) does not match 'those-dashes-dont-match'

보다 일반적인 경우에는 이러한 개체를 만드는 데 Use를 사용할 수 있습니다. Use는 유효성을 검사하는 동안 값을 변환하는 동시에 값을 검증하는 데 도움이 됩니다.

>>> from schema import Use

>>> Schema(Use(int)).validate('123')
123

>>> Schema(Use(lambda f: open(f, 'a'))).validate('LICENSE-MIT')
<_io.TextIOWrapper name='LICENSE-MIT' mode='a' encoding='UTF-8'>

Detail한 항목을 삭제하면 Use는 기본적으로 다음과 같습니다.

class Use(object):

    def __init__(self, callable_):
        self._callable = callable_

    def validate(self, data):
        try:
            return self._callable(data)
        except Exception as e:
            raise SchemaError('%r raised %r' % (self._callable.__name__, e))

데이터의 일부를 변환하고 유효성을 검사해야 하지만 원본 데이터는 변경되지 않은 상태로 유지해야 하는 경우가 있습니다. Const는 데이터를 안전하게 유지하는 데 도움이 됩니다.

>> from schema import Use, Const, And, Schema

>> from datetime import datetime

>> is_future = lambda date: datetime.now() > date

>> to_json = lambda v: {"timestamp": v}

>> Schema(And(Const(And(Use(datetime.fromtimestamp), is_future)), Use(to_json))).validate(1234567890)
{"timestamp": 1234567890}

이제 고유한 validation-aware classe와 data types를 작성할 수 있습니다.

Lists, similar containers

Schema(…)가 list, tuple, set 또는 frozenset의 인스턴스를 만나면 해당 컨테이너 내부에 나열된 모든 스키마에 대해 해당 데이터 컨테이너의 내용을 확인하고 모든 오류를 집계합니다.

>>> Schema([1, 0]).validate([1, 1, 0, 1])
[1, 1, 0, 1]

>>> Schema((int, float)).validate((5, 7, 8, 'not int or float here'))
Traceback (most recent call last):
...
schema.SchemaError: Or(<class 'int'>, <class 'float'>) did not validate 'not int or float here'
'not int or float here' should be instance of 'int'
'not int or float here' should be instance of 'float'

Dictionaries

Schema(…)가 dict의 인스턴스를 만나면 데이터 키-값 쌍의 유효성을 검사합니다.

>>> d = Schema({'name': str,
...             'age': lambda n: 18 <= n <= 99}).validate({'name': 'Sue', 'age': 28})

>>> assert d == {'name': 'Sue', 'age': 28}

키를 스키마로 지정할 수도 있습니다.

>>> schema = Schema({str: int,  # string keys should have integer values
...                  int: None})  # int keys should be always None

>>> data = schema.validate({'key1': 1, 'key2': 2,
...                         10: None, 20: None})

>>> schema.validate({'key1': 1,
...                   10: 'not None here'})
Traceback (most recent call last):
...
schema.SchemaError: Key '10' error:
None does not match 'not None here'

이것은 특정 키-값을 확인하고 싶지만 다른 키-값은 신경쓰지 않는 경우에 유용합니다.

>>> schema = Schema({'<id>': int,
...                  '<file>': Use(open),
...                  str: object})  # don't care about other str keys

>>> data = schema.validate({'<id>': 10,
...                         '<file>': 'README.rst',
...                         '--verbose': True})

다음과 같이 키를 선택 사항으로 표시할 수 있습니다.

>>> from schema import Optional
>>> Schema({'name': str,
...         Optional('occupation'): str}).validate({'name': 'Sam'})
{'name': 'Sam'}

Optional 키는 데이터의 키가 일치하지 않을 때 사용되는 기본값을 전달할 수도 있습니다.

>>> from schema import Optional
>>> Schema({Optional('color', default='blue'): str,
...         str: str}).validate({'texture': 'furry'}
...       ) == {'color': 'blue', 'texture': 'furry'}
True

default는 값에 지정된 validators를 통해 전달되지 않고 그대로 사용됩니다.

default는 호출 가능합니다.

>>> from schema import Schema, Optional
>>> Schema({Optional('data', default=dict): {}}).validate({}) == {'data': {}}
True

주의사항: 유형을 지정하면 스키마가 빈 dict의 유효성을 검사하지 않습니다.

>>> Schema({int:int}).is_valid({})
False

그렇게 하려면 Schema(Or({int:int}, {})) 가 필요합니다. 이것은 Schema([int]).is_valid([]) 가 True를 반환하는 목록에서 발생하는 것과 다릅니다.

스키마에는 동일한 데이터에 대해 여러 스키마의 유효성을 검사하는 데 도움이 되는 And 및 Or클래스가 있습니다.

>>> from schema import And, Or

>>> Schema({'age': And(int, lambda n: 0 < n < 99)}).validate({'age': 7})
{'age': 7}

>>> Schema({'password': And(str, lambda s: len(s) > 6)}).validate({'password': 'hai'})
Traceback (most recent call last):
...
schema.SchemaError: Key 'password' error:
<lambda>('hai') should evaluate to True

>>> Schema(And(Or(int, float), lambda x: x > 0)).validate(3.1415)
3.1415

dict에서 두 개의 키를 “하나 또는 다른" 방식으로 결합할 수도 있습니다. 이렇게 하려면 OR 클래스를 키로 사용합니다.

>>> from schema import Or, Schema
>>> schema = Schema({
...    Or("key1", "key2", only_one=True): str
... })

>>> schema.validate({"key1": "test"}) # Ok
{'key1': 'test'}

>>> schema.validate({"key1": "test", "key2": "test"}) # SchemaError
Traceback (most recent call last):
...
schema.SchemaOnlyOneAllowedError: There are multiple keys present from the Or('key1', 'key2')

Hooks

유효한 키:값이 발견될 때마다 실행되는 함수인 Hook을 정의할 수 있습니다. Forbidden 클래스가 그 예입니다.

>>> from schema import Forbidden
>>> Schema({Forbidden('age'): object}).validate({'age': 50})
Traceback (most recent call last):
...
schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50}

몇 가지는 주목할 가치가 있습니다. 첫 째, 금지된 키와 쌍을 이루는 값에 따라 reject여부가 결정됩니다.

>>> Schema({Forbidden('age'): str, 'age': int}).validate({'age': 50})
{'age': 50}

참고 : 여기서 ‘age’키를 제공하지 않았다면 호출도 실패했지만 SchemaForbiddenKeyError가 아닌 SchemaWrongKeyError로 실패했을 것입니다.

둘째, Forbidden은 표준 key보다 우선 순위가 높으므로 결과적으로 Optional보다 우선 순위가 높습니다. 이것은 우리가 할 수 있음을 의미합니다.

>>> Schema({Forbidden('age'): object, Optional(str): object}).validate({'age': 50})
Traceback (most recent call last):
...
schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50}

고유한 hook를 정의할 수도 있습니다. 다음 hook은 키가 발견되면 _my_function을 호출합니다.

from schema import Hook
def _my_function(key, scope, error):
    print(key, scope, error)

Hook("key", handler=_my_function)

다음은 키가 발생할 때마다 경고를 기록하기 위해 Deprecated 클래스가 추가되는 예입니다.

from schema import Hook, Schema
class Deprecated(Hook):
    def __init__(self, *args, **kwargs):
        kwargs["handler"] = lambda key, *args: logging.warn(f"`{key}` is deprecated. " + (self._error or ""))
        super(Deprecated, self).__init__(*args, **kwargs)

Schema({Deprecated("test", "custom error message."): object}, ignore_extra_keys=True).validate({"test": "value"})
...
WARNING: `test` is deprecated. custom error message.

Extra Keys

Schema(…) 매개 변수 ignore_extra_keys를 사용하면 검증에서 dict의 추가 키를 무시하고 유효성 검사 후 반환하지 않도록 합니다.

>>> schema = Schema({'name': str}, ignore_extra_keys=True)
>>> schema.validate({'name': 'Sam', 'age': '42'})
{'name': 'Sam'}

추가 키를 반환하려면 object: object를 키/값 쌍 중 하나로 사용합니다. object는 모든 key : value가 일치합니다. 그렇지 않으면 추가 키가 SchemaError를 발생시킵니다.

Customized Validation

Schema.validation 메서드는 추가 keyword arguments를 허용합니다.

keyword arguments는 모든 하위 유효성 검사 기능 항목(임시 Schema 개체 포함)의 validate 메소드로 전파되거나 optional 키에 대한 기본값 호출 가능 항목(호출 가능 항목이 지정된 경우)로 전파됩니다.

이 기능은 사용자 정의 유효성 검사를 위해 Schema 클래스의 상속과 함께 사용할 수 있습니다.

다음은 더 큰 스키마의 하위 스키마에 대해 유효성 검사 후에 실행되는 post-validation hook의 예시입니다.

class EventSchema(schema.Schema):

    def validate(self, data, _is_event_schema=True):
        data = super(EventSchema, self).validate(data, _is_event_schema=False)
        if _is_event_schema and data.get("minimum", None) is None:
            data["minimum"] = data["capacity"]
        return data

events_schema = schema.Schema(
    {
        str: EventSchema({
            "capacity": int,
            schema.Optional("minimum"): int,  # default to capacity
        })
    }
)

data = {'event1': {'capacity': 1}, 'event2': {'capacity': 2, 'minimum': 3}}
events = events_schema.validate(data)

assert events['event1']['minimum'] == 1  # == capacity
assert events['event2']['minimum'] == 3

추가 keyword arguments _is_event_schema 는 사용자 정의된 동작을 EventSchema 개체 자체로 제한하는데 필요하므로 자식 스키마에 대한 self.__class__.validate의 재귀 호출에 영향을 주지 않습니다.

예시(Schema("capacity").validate("capacity"))

User-friendly error reporting

keyword arguments error를 유효한 클래스(예시 : Schema, And, Or, Regex, Use)에 전달하여 기본 제공 클래스 대신 이 오류를 report할 수 있습니다.

>>> Schema(Use(int, error='Invalid year')).validate('XVII')
Traceback (most recent call last):
...
schema.SchemaError: Invalid year

자동 생성된 오류 메시지의 경우 에외의 exc.autos 에 액세스하고 error 텍스트가 전달된 오류의 경우 exc.errors에 액세스하여 발생한 모든 오류를 볼 수 있습니다.

추적없이 사용자에게 메시지를 표시하려면 sys.exe(ex.code)를 사용하여 종료할 수 있습니다. 이 경우 error 메시지가 우선 적용됩니다.

A JSON API example

아래에 나와있는 예시는 Github API에서 gist요청을 만드는 것의 유효성검사입니다.

gist url : https://docs.github.com/en/rest/gists

>>> gist = '''{"description": "the description for this gist",
...            "public": true,
...            "files": {
...                "file1.txt": {"content": "String file contents"},
...                "other.txt": {"content": "Another file contents"}}}'''

>>> from schema import Schema, And, Use, Optional

>>> import json

>>> gist_schema = Schema(And(Use(json.loads),  # first convert from JSON
...                          # use str since json returns unicode
...                          {Optional('description'): str,
...                           'public': bool,
...                           'files': {str: {'content': str}}}))

>>> gist = gist_schema.validate(gist)

# gist:
{u'description': u'the description for this gist',
 u'files': {u'file1.txt': {u'content': u'String file contents'},
            u'other.txt': {u'content': u'Another file contents'}},
 u'public': True}

 

 

참고 링크
https://pypi.org/project/schema/

 

schema

Simple data validation library

pypi.org

 

댓글