- To get rid of code snippet like these (... cumbersome and tedious validation)
def do_something(params): val_a_must_int = params.get('a', 0) val_b_must_be_non_empty_list = params.get('b', []) # if key c presents, value c must be a date string between '2000-01-01' to '2020-01-01' val_c_might_be_none = params.get('c', None) # check type if type(val_a_must_int) != int: raise XXX # check type & value if type(val_b_must_list) != list or len(val_b_must_be_non_empty_list) == 0: raise XXX # if value exists, check its value if val_c_might_be_none is not None: date_c = datetime.strptime(val_c_might_be_present, '%Y-%m-%d') date_20000101 = datetime.date(2000, 1, 1) date_20200101 = datetime.date(2020, 1, 1) if not (date_20000101 <= date_c <= date_20200101): raise XXX ... # do something actually
- Basic usage:
pip install data-spec-validator
- Advance usage (decorator)
- The decorator function
dsvmay depend onDjango(support v3.0 or later) ordjangorestframework.
- The decorator function
pip install data-spec-validator[decorator-dj] # Django Only pip install data-spec-validator[decorator] # Django Rest Framework
- Do
validate_data_specdirectly wherever you like
from data_spec_validator.spec import INT, DIGIT_STR, ONE_OF, LIST_OF, Checker, CheckerOP, validate_data_spec class SomeSpec: field_a = Checker([INT]) field_b = Checker([DIGIT_STR], optional=True) field_c = Checker([DIGIT_STR, INT], op=CheckerOP.ANY) filed_d_array = Checker([LIST_OF], LIST_OF=int, alias='field_d[]', optional=True) some_data = dict(field_a=4, field_b='3', field_c=1, field_dont_care=[5,6]) validate_data_spec(some_data, SomeSpec) # return True some_data = dict(field_a=4, field_c='1') validate_data_spec(some_data, SomeSpec) # return True some_data = { 'field_a': 4, 'field_c': 1, 'field_d[]': [5, 6], } validate_data_spec(some_data, SomeSpec) # return True some_data = dict(field_a='4', field_c='1') validate_data_spec(some_data, SomeSpec) # raise Exception some_data = dict(field_a='4', field_c='1') validate_data_spec(some_data, SomeSpec, nothrow=True) # return False class AnotherSpec: field = Checker([ONE_OF], ONE_OF=[1, '2', [3, 4], {'5': 6}]) another_data = dict(field=[3, 4]) validate_data_spec(another_data, AnotherSpec) # return True another_data = dict(field='4') validate_data_spec(another_data, AnotherSpec) # raise Exception
- Multiple rows data
from data_spec_validator.spec import INT, STR, Checker, validate_data_spec class SingleSpec: f_a = Checker([INT]) f_b = Checker([STR]) multirow_data = [dict(f_a=1, f_b='1'), dict(f_a=2, f_b='2'), dict(f_a=3, f_b='3')] validate_data_spec(multirow_data, SingleSpec, multirow=True) # return True
int_field = Checker([INT]) or Checker[int]
float_field = Checker([FLOAT]) or Checker([float])
number_field = Checker([NUMBER])
str_field = Checker([STR]) or Checker([str])
digi_str_field = Checker([DIGIT_STR])
bool_field = Checker([BOOL]) or Checker([bool])
dict_field = Checker([DICT]) or Checker([dict])
list_field = Checker([LIST]) or Checker([list])
date_obj_field = Checker([DATE_OBJECT]) or Checker([datetime.date])
datetime_obj_field = Checker([DATETIME_OBJECT]) or Checker([datetime.datetime])
none_field = Checker([NONE]) or Checker([type(None)])
json_field = Checker([JSON])
json_bool_field = Checker([JSON_BOOL])
one_of_field = Checker([ONE_OF], ONE_OF=['a', 'b', 'c'])
spec_field = Checker([SPEC], SPEC=SomeSpecClass)
list_of_int_field = Checker([LIST_OF], LIST_OF=INT)
list_of_spec_field = Checker([LIST_OF], LIST_OF=SomeSpecClass)
length_field = Checker([LENGTH], LENGTH=dict(min=3, max=5))
amount_field = Checker([AMOUNT])
amount_range_field = Checker([AMOUNT_RANGE], AMOUNT_RANGE=dict(min=-2.1, max=3.8))
decimal_place_field = Checker([DECIMAL_PLACE], DECIMAL_PLACE=4)
date_field = Checker([DATE])
date_range_field = Checker([DATE_RANGE], DATE_RANGE=dict(min='2000-01-01', max='2010-12-31'))
email_field = Checker([EMAIL])
uuid_field = Checker([UUID]) or Checker([uuid.UUID])
re_field = Checker([REGEX], REGEX=dict(pattern=r'^The'))
re_field = Checker([REGEX], REGEX=dict(pattern=r'watch out', method='match'))
If a exists, c must not exist, if b exists, a must exist, if c exists, a must not exist.
Practically, optional=True will be configured in the most use cases, FMI, see test/test_spec.py
a = Checker([COND_EXIST], optional=True, COND_EXIST=dict(WITHOUT=['c']))
b = Checker([COND_EXIST], optional=True, COND_EXIST=dict(WITH=['a']))
c = Checker([COND_EXIST], optional=True, COND_EXIST=dict(WITHOUT=['a']))
class SomeClass:
pass
a = Checker([SomeClass])
- Decorate a method with
dsv, the method must meet one of the following requirements.- It's a view's member function, and the view has a WSGIRequest(
django.core.handlers.wsgi.WSGIRequest) attribute. - It's a view's member function, and the 2nd argument of the method is a
rest_framework.request.Requestinstance. - It's already decorated with
rest_framework.decorators import api_view, the 1st argument is arest_framework.request.Request
- It's a view's member function, and the view has a WSGIRequest(
from rest_framework.decorators import api_view from rest_framework.views import APIView from data_spec_validator.decorator import dsv from data_spec_validator.spec import UUID, EMAIL, Checker class SomeViewSpec: param_a = Checker([UUID]) param_b = Checker([EMAIL]) class SomeView(APIView): @dsv(SomeViewSpec) def get(self, request): pass @api_view(('POST',)) @dsv(SomeViewSpec) def customer_create(request): pass @api_view(('POST',)) @dsv(SomeViewSpec, multirow=True) # For type(request.POST) is list def customer_create(request): pass
- Decorate another method with
dsv_request_metacan help you validate the META in request header.
- Define custom CHECK constant (
gt_checkin this case) and write custom Validator(GreaterThanValidatorin this case)
gt_check = 'gt_check' from data_spec_validator.spec.defines import BaseValidator class GreaterThanValidator(BaseValidator): name = gt_check @staticmethod def validate(value, extra, data): criteria = extra.get(GreaterThanValidator.name) return value > criteria, ValueError(f'{value} is not greater than {criteria}')
- Register custom check & validator into data_spec_validator
from data_spec_validator.spec import custom_spec, Checker, validate_data_spec custom_spec.register(dict(gt_check=GreaterThanValidator())) class GreaterThanSpec: key = Checker(['gt_check'], GT_CHECK=10) ok_data = dict(key=11) validate_data_spec(ok_data, GreaterThanSpec) # return True nok_data = dict(key=9) validate_data_spec(ok_data, GreaterThanSpec) # raise Exception
- 2 modes (Default v.s. Vague), can be switched by calling
reset_msg_level(vague=True)
# In default mode, any exception happens, there will be a reason in the message "field: XXX, reason: '3' is not a integer" # In vague mode, any exception happens, a general message is shown "field: XXX not well-formatted"
- A spec class decorated with
dsv_feature(strict=True)detects unexpected key/value in data
from data_spec_validator.spec import Checker, validate_data_spec, dsv_feature, BOOL @dsv_feature(strict=True) class StrictSpec: a = Checker([BOOL]) ok_data = dict(a=True) validate_data_spec(ok_data, StrictSpec) # return True nok_data = dict(a=True, b=1) validate_data_spec(nok_data, StrictSpec) # raise Exception
- A spec class decorated with e.g.
dsv_feature(any_keys_set={...})means that at least one key among a keys tuple from the set must exist.
from data_spec_validator.spec import Checker, validate_data_spec, dsv_feature, INT @dsv_feature(any_keys_set={('a', 'b'), ('c', 'd')}) class _AnyKeysSetSpec: a = Checker([INT], optional=True) b = Checker([INT], optional=True) c = Checker([INT], optional=True) d = Checker([INT], optional=True) validate_data_spec(dict(a=1, c=1, d=1), _AnyKeysSetSpec) validate_data_spec(dict(a=1, c=1), _AnyKeysSetSpec) validate_data_spec(dict(a=1, d=1), _AnyKeysSetSpec) validate_data_spec(dict(b=1, c=1, d=1), _AnyKeysSetSpec) validate_data_spec(dict(b=1, c=1), _AnyKeysSetSpec) validate_data_spec(dict(b=1, d=1), _AnyKeysSetSpec) validate_data_spec(dict(a=1, b=1, c=1), _AnyKeysSetSpec) validate_data_spec(dict(a=1, b=1, d=1), _AnyKeysSetSpec) validate_data_spec(dict(a=1, b=1, c=1, d=1), _AnyKeysSetSpec) validate_data_spec(dict(a=1), _AnyKeysSetSpec) # raise exception validate_data_spec(dict(b=1), _AnyKeysSetSpec) # raise exception validate_data_spec(dict(c=1), _AnyKeysSetSpec) # raise exception validate_data_spec(dict(d=1), _AnyKeysSetSpec) # raise exception validate_data_spec(dict(e=1), _AnyKeysSetSpec) # raise exception
NOTE 1: ErrorMode.MSE stands for MOST-SIGNIFICANT-ERROR
NOTE 2: The validation results respect to the ErrorMode feature config on the OUTER-MOST spec. All nested specs
follow the OUTER-MOST spec configuration, for more reference, see test_spec.py:test_err_mode
from data_spec_validator.spec import Checker, validate_data_spec, dsv_feature, LENGTH, STR, AMOUNT, ErrorMode, INT, DIGIT_STR @dsv_feature(err_mode=ErrorMode.ALL) class _ErrModeAllSpec: a = Checker([INT]) b = Checker([DIGIT_STR]) c = Checker([LENGTH, STR, AMOUNT], LENGTH=dict(min=3, max=5)) nok_data = dict( a=True, b='abc', c='22', ) validate_data_spec(nok_data, _ErrModeAllSpec) # raise DSVError """ A DSVError is raised with 3 errors in args. (TypeError('field: _ErrModeAllSpec.a, reason: True is not an integer',), TypeError("field: _ErrModeAllSpec.b, reason: 'abc' is not a digit str",), ValueError("field: _ErrModeAllSpec.c, reason: Length of '22' must be between 3 and 5",)) """
from data_spec_validator.spec import Checker, dsv_feature, validate_data_spec, INT @dsv_feature(spec_name='CustomSpecName') class _MySpec: a = Checker([INT]) nok_data = dict( a='abc', ) validate_data_spec(nok_data, _MySpec) """ TypeError: field: CustomSpecName.a, reason: 'abc' is not an integer """
python -m unittest test/*.*