163 lines
4.0 KiB
Python
163 lines
4.0 KiB
Python
try:
|
|
import cerberus
|
|
except ImportError:
|
|
cerberus = None
|
|
|
|
|
|
class ValidationError(ValueError):
|
|
def __init__(self, value, validator):
|
|
super(ValidationError, self).__init__(value)
|
|
self.validator = validator
|
|
self.value = value
|
|
|
|
def __str__(self):
|
|
return '{}\n{}'.format(
|
|
self.value,
|
|
'\n'.join(
|
|
'{}: {}'.format(k, e)
|
|
for k, errors in self.validator.errors.items()
|
|
for e in errors
|
|
)
|
|
)
|
|
|
|
|
|
VALIDATORS = {}
|
|
|
|
|
|
def validate(cls, data):
|
|
if not cerberus: # Skip validation if Cerberus is not available.
|
|
return
|
|
schema = cls.__SCHEMA__
|
|
key = id(schema)
|
|
try:
|
|
v = VALIDATORS[key]
|
|
except KeyError:
|
|
v = VALIDATORS[key] = cerberus.Validator(schema, allow_unknown=True)
|
|
if v.validate(data, normalize=False):
|
|
return
|
|
raise ValidationError(data, v)
|
|
|
|
|
|
class DataView(object):
|
|
"""A "view" to a data.
|
|
|
|
Validates the input mapping on creation. A subclass is expected to
|
|
provide a `__SCHEMA__` class attribute specifying a validator schema.
|
|
"""
|
|
|
|
def __init__(self, data):
|
|
self.validate(data)
|
|
self._data = data
|
|
|
|
def __repr__(self):
|
|
return "{0}({1!r})".format(type(self).__name__, self._data)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, type(self)):
|
|
raise TypeError(
|
|
"cannot compare {0!r} with {1!r}".format(
|
|
type(self).__name__, type(other).__name__
|
|
)
|
|
)
|
|
return self._data == other._data
|
|
|
|
def __getitem__(self, key):
|
|
return self._data[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
self._data[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
del self._data[key]
|
|
|
|
def get(self, key, default=None):
|
|
try:
|
|
return self[key]
|
|
except KeyError:
|
|
return default
|
|
|
|
@classmethod
|
|
def validate(cls, data):
|
|
return validate(cls, data)
|
|
|
|
|
|
class DataViewCollection(DataView):
|
|
"""A homogeneous collection of data views.
|
|
|
|
Subclasses are expected to assign a class attribute `item_class` to specify
|
|
the type of items it contains. This class will be used to coerce return
|
|
values when accessed. The item class should conform to the `DataView`
|
|
protocol.
|
|
|
|
You should not instantiate an instance from this class, but from one of its
|
|
subclasses instead.
|
|
"""
|
|
|
|
item_class = None
|
|
|
|
def __repr__(self):
|
|
return "{0}({1!r})".format(type(self).__name__, self._data)
|
|
|
|
def __len__(self):
|
|
return len(self._data)
|
|
|
|
def __getitem__(self, key):
|
|
return self.item_class(self._data[key])
|
|
|
|
def __setitem__(self, key, value):
|
|
if isinstance(value, DataView):
|
|
value = value._data
|
|
self._data[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
del self._data[key]
|
|
|
|
|
|
class DataViewMapping(DataViewCollection):
|
|
"""A mapping of data views.
|
|
|
|
The keys are primitive values, while values are instances of `item_class`.
|
|
"""
|
|
|
|
@classmethod
|
|
def validate(cls, data):
|
|
for d in data.values():
|
|
cls.item_class.validate(d)
|
|
|
|
def __iter__(self):
|
|
return iter(self._data)
|
|
|
|
def keys(self):
|
|
return self._data.keys()
|
|
|
|
def values(self):
|
|
return [self[k] for k in self._data]
|
|
|
|
def items(self):
|
|
return [(k, self[k]) for k in self._data]
|
|
|
|
|
|
class DataViewSequence(DataViewCollection):
|
|
"""A sequence of data views.
|
|
|
|
Each entry is an instance of `item_class`.
|
|
"""
|
|
|
|
@classmethod
|
|
def validate(cls, data):
|
|
for d in data:
|
|
cls.item_class.validate(d)
|
|
|
|
def __iter__(self):
|
|
return (self.item_class(d) for d in self._data)
|
|
|
|
def __getitem__(self, key):
|
|
if isinstance(key, slice):
|
|
return type(self)(self._data[key])
|
|
return super(DataViewSequence, self).__getitem__(key)
|
|
|
|
def append(self, value):
|
|
if isinstance(value, DataView):
|
|
value = value._data
|
|
self._data.append(value)
|