Source code for req.schema.property

"""This module contains common :class:`Properties<.Property>` that can be used
inside of a :class:`.Schema`.

:example:
  >>> class ASchema(Schema):
  ...   name = String()
  ...   age = Number(_optional=True)
  ...
  >>> v.validate({})
  Traceback (most recent call last):
    ...
  ValidationError: ['name: missing']
  >>> v.validate({"name": "John Doe"})
  >>> v.validate({"name": "John Doe", "age": 24})
  >>> v.validate({"name": "John Doe", "age": "42"})
  Traceback (most recent call last):
    ...
  ValidationError: ['age: invalid number']
"""
import dateutil.tz
import numbers
import random
import re
import socket
import string
import six

from copy import deepcopy
import datetime
from faker import Faker
from functools import partial
from six.moves import range, zip

from .datetime_utils import parse_datetime, parse_date
from .schema import SchemaLike, Property, ValidationError

alphanumeric = string.ascii_letters + string.digits
fake = Faker()


class SequenceLike(Property):
    def update(self, values):
        if values is None:
            self.validate(values)
            self._value = None
            return

        materialized_values = []
        for value in values:
            if isinstance(value, SchemaLike):
                materialized_values.append(value.materialize())
            else:
                materialized_values.append(value)

        self.validate(materialized_values)
        self._value = materialized_values


[docs]class Tuple(SequenceLike): r"""A property for fixed length containers. Examples: >>> from req.schema import Tuple, String, Number >>> t = Tuple(String(), String(), Number()) >>> t.validate(["a", "b", 1]) >>> t.validate([1, "b", 1]) Traceback (most recent call last): ... ValidationError: ['invalid string at index 0'] >>> t.validate(["a", "b", 1, 2]) Traceback (most recent call last): ... ValidationError: ['bad tuple size, expected 3 items'] Parameters: *properties(SchemaLike list): A list of Schema representing the elements of the tuple. """ def __init__(self, *properties, **options): super(Tuple, self).__init__(**options) self.properties = deepcopy(properties) def _validate(self, data): if not isinstance(data, (tuple, list)): raise ValidationError("invalid tuple") size = len(self.properties) if len(data) != size: raise ValidationError("bad tuple size, expected {} items".format(size)) errors = [] for i, (item, property) in enumerate(zip(data, self.properties)): try: property.validate(item) except ValidationError as e: errors.append("{} at index {}".format(e.message, i)) if errors: raise ValidationError(errors) def _generate(self): return tuple(p.generate() for p in self.properties)
[docs]class Dictionary(Property): r"""A Dictionary property. Examples: >>> from req.schema import Dictionary, String, Number >>> d = Dictionary(a=String(), b=Number()) >>> d.validate({"a": "hello", "b": 42}) >>> d.validate({"a": 24, "b": 42}) Traceback (most recent call last): ... ValidationError: ['a: invalid string'] Parameters: **properties(dict): A dictionary representing each key and its schema in the input data. If omitted, arbitrary dictionaries will be accepted. """ def __init__(self, _name=None, _description=None, _optional=None, **properties): super(Dictionary, self).__init__( _name=_name, _description=_description, _optional=_optional ) self.properties = deepcopy(properties) def _validate(self, data): if not isinstance(data, dict): raise ValidationError("invalid dictionary") errors = [] for key, property in six.iteritems(self.properties): if key not in data: errors.append("{}: missing".format(key)) continue try: property.validate(data.get(key)) except ValidationError as e: errors.append("{}: {}".format(key, e.message)) if errors: raise ValidationError(errors) def _generate(self): return {k: p.generate() for k, p in six.iteritems(self.properties)}
[docs]class ValueDictionary(Property): """A :class:`.Dictionary` variant that validates all values against the specified :class:`.SchemaLike`. Examples: >>> from req.schema import Number, ValueDictionary >>> v = ValueDictionary(Number()) >>> v.validate({"a": 1, "b": 2}) >>> v.validate({"a": "hello"}) Traceback (most recent call last): ... ValidationError: ['a: invalid number'] Parameters: _item_property(SchemaLike): A SchemaLike representing what shape each value in the dictionary must have. """ def __init__(self, _item_property=None, _name=None, _description=None, _optional=None): super(ValueDictionary, self).__init__( _name=_name, _description=_description, _optional=_optional ) self.item_property = deepcopy(_item_property) def _validate(self, data): if not isinstance(data, dict): raise ValidationError("invalid dictionary") errors = [] for key, value in six.iteritems(data): try: self.item_property.validate(value) except ValidationError as e: errors.append("{}: {}".format(key, e.message)) if errors: raise ValidationError(errors) def _generate(self): return {fake.slug(): self.item_property.generate() for _ in range(random.randint(0, 10))}
[docs]class List(SequenceLike): """A property for lists. Examples: >>> from req.schema import List, Integer >>> l = List() >>> l.validate([]) >>> l.validate(["a", 1, [2, 3]]) >>> l = List(Integer()) >>> l.validate([]) >>> l.validate([1, 2, 3]) >>> l.validate(["a", 1]) Traceback (most recent call last): .. ValidationError: ['0: invalid integer'] Parameters: _item_property(SchemaLike): If provided, then every element in the list will be expected to validate against that Schema. If omitted, this will accept any list. _min_items(int): The minimum number of items the list can contain. _max_items(int): The maximum number of items the list can contain. _unique_items(bool): Whether or not every item in the list must be unique. """ def __init__(self, _item_property=None, _min_items=None, _max_items=None, _unique_items=None, **options): super(List, self).__init__(**options) self.min_items = _min_items self.max_items = _max_items self.unique_items = _unique_items self._metadata += ("min_items", "max_items", "unique_items") self.item_property = deepcopy(_item_property) def _validate(self, data): if not isinstance(data, list): raise ValidationError("invalid list") if self.min_items is not None and len(data) < self.min_items: raise ValidationError("item count must be >= {}".format(self.min_items)) if self.max_items is not None and len(data) > self.max_items: raise ValidationError("item count must be <= {}".format(self.max_items)) if self.unique_items is not None and len(data) != len(set(data)): raise ValidationError("all items must be unique") if self.item_property is not None: errors = [] for i, item in enumerate(data): try: self.item_property.validate(item) except ValidationError as e: errors.append("{}: {}".format(i, e.message)) if errors: raise ValidationError(errors) def _generate(self): min_items = 0 if self.min_items is None else self.min_items max_items = max(min_items, 3 if self.max_items is None else self.max_items) item_count = random.randint(min_items, max_items) items = [self.item_property.generate() for _ in range(item_count)] if self.unique_items: unique_items = set(items) while len(unique_items) < item_count: unique_items.add(self.item_property.generate()) items = list(unique_items) return items
class _BasicNumber(Property): def _validate(self, data): if not isinstance(data, numbers.Number) or isinstance(data, bool): raise ValidationError("invalid number")
[docs]class Number(_BasicNumber): """A property for numbers. Examples: >>> from req.schema import Number >>> n = Number() >>> n.validate(1) >>> n.validate(1.25) >>> n.validate("a") Traceback (most recent call last): ... ValidationError: ['invalid number'] Parameters: _minimum(int|float): The minimum value the number can have. _maximum(int|float): The maximum value the number can have. _multiple_of(int|float): A value the number must be a multiple of. """ def __init__(self, _minimum=None, _maximum=None, _multiple_of=None, **options): super(Number, self).__init__(**options) self.minimum = _minimum self.maximum = _maximum self.multiple_of = _multiple_of self._metadata += ("minimum", "maximum", "multiple_of") def _validate(self, data): super(Number, self)._validate(data) if self.minimum is not None and data < self.minimum: raise ValidationError("must be >= {}".format(self.minimum)) if self.maximum is not None and data > self.maximum: raise ValidationError("must be <= {}".format(self.maximum)) if self.multiple_of is not None and data % self.multiple_of != 0: raise ValidationError("must be a multiple of {}".format(self.multiple_of)) def _generate(self): minimum = -2 ** 16 if self.minimum is None else self.minimum maximum = +2 ** 16 - 1 if self.maximum is None else self.maximum value = random.uniform(minimum, maximum) if self.multiple_of is not None: return self.multiple_of return max(minimum, min(maximum, value))
[docs]class Integer(Number): """Like :class:`.Number` but specifically for integers. Examples: >>> from req.schema import Integer >>> i = Integer() >>> i.validate(1) >>> i.validate(1.0) Traceback (most recent call last): ... ValidationError: ['invalid integer'] """ def _validate(self, data): if not isinstance(data, six.integer_types): raise ValidationError("invalid integer") return super(Integer, self)._validate(data) def _generate(self): minimum = -2 ** 16 if self.minimum is None else self.minimum maximum = +2 ** 16 - 1 if self.maximum is None else self.maximum value = random.randint(minimum, maximum) if self.multiple_of is not None: return self.multiple_of return max(minimum, min(maximum, value))
[docs]class Boolean(_BasicNumber): """A property for booleans. Examples: >>> from req.schema import Boolean >>> b = Boolean() >>> b.validate(False) >>> b.validate(True) >>> b.validate(1) Traceback (most recent call last): ... ValidationError: ['invalid boolean'] """ def _validate(self, data): if not isinstance(data, bool): raise ValidationError("invalid boolean") def _generate(self): return bool(random.randint(0, 1))
[docs]class String(Property): """A property for strings. Examples: >>> from req.schema import String >>> s = String() >>> s.validate("hello") >>> s.validate(1) Traceback (most recent call last): ... ValidationError: ['invalid string'] >>> s = String(_choices=("a", "b", "c")) >>> s.validate("a") >>> s.validate("d") Traceback (most recent call last): ... ValidationError: ['must be one of ("a", "b", "c")'] Parameters: _min_length(int): The minimum length of the string. _max_length(int): The maximum length of the string. _choices(str list): A constraint for the possible string values of this property. """ def __init__(self, _min_length=None, _max_length=None, _choices=None, **options): super(String, self).__init__(**options) self.choices = _choices self.min_length = _min_length self.max_length = _max_length self._metadata += ("min_length", "max_length") def _validate(self, data): if not isinstance(data, six.string_types): raise ValidationError("invalid string") if self.min_length is not None and len(data) < self.min_length: raise ValidationError("length must be >= {}".format(self.min_length)) if self.max_length is not None and len(data) > self.max_length: raise ValidationError("length must be <= {}".format(self.max_length)) if self.choices is not None and data not in self.choices: raise ValidationError("must be one of {}".format(self.choices)) def _generate(self): if self.choices: return random.choice(self.choices) # Generate nicer strings for properties that might be names. if self.min_length is None and ( self.max_length is None or self.max_length > 50 ): return fake.slug() return fake.pystr(min_chars=self.min_length, max_chars=self.max_length or 20)
[docs]class Regexp(String): r"""A property for regular expressions. Examples: >>> from req.schema import Regexp >>> r = Regexp(r"\d+") >>> r.validate("123") >>> r.validate("a") Traceback (most recent call last): ... ValidationError: ['must match regexp "\\d+"'] Parameters: expression(str): The regular expression to validate values against. """ def __init__(self, _expression, **options): super(Regexp, self).__init__(**options) self.expression = _expression self._metadata = ("_description",) def _validate(self, data): super(Regexp, self)._validate(data) if not re.match(self.expression, data): raise ValidationError("must match regexp {!r}".format(self.expression)) def _generate(self): return ""
class Password(String): """A property for password validation. Validation defaults are set for minimum basic PCI compliance with overridable fields. Examples: >>> from req.schema import Password >>> p = Password() >>> p.validate("weak") Traceback (most recent call last): ... ValidationError: ['length must be >= 8'] >>> p.validate("lessweak123") Traceback (most recent call last): ... ValidationError: ['must have at least 1 uppercase characters'] >>> p.validate("stillprettyWeak1") Parameters: _min_length(int): The minimum number of characters. _max_length(int): The minimum number of characters. _min_digit(int): The minimum number of digits. _min_lower(int): The minimum number of lowercase characters. _min_upper(int): The minimum number of uppercase characters. _min_special(int): The minimum number of special characters. """ def __init__(self, _min_length=8, _min_digit=1, _min_lower=1, _min_upper=1, _min_special=None, **options): super(Password, self).__init__(_min_length=_min_length, **options) self.min_digit = _min_digit self.min_lower = _min_lower self.min_upper = _min_upper self.min_special = _min_special self._metadata += ( "_description", "min_digit", "min_lower", "min_upper", "min_special" ) def _validate(self, data): super(Password, self)._validate(data) if self.min_lower is not None and len(re.findall("[a-z]", data)) < self.min_lower: raise ValidationError("must have at least {} lowercase characters".format(self.min_lower)) if self.min_upper is not None and len(re.findall("[A-Z]", data)) < self.min_upper: raise ValidationError("must have at least {} uppercase characters".format(self.min_upper)) if self.min_digit is not None and len(re.findall("[0-9]", data)) < self.min_digit: raise ValidationError("must have at least {} digits".format(self.min_digit)) if self.min_special is not None and len(re.findall(r"[\W]", data)) < self.min_special: raise ValidationError("must have at least {} special characters".format(self.min_special)) def _generate(self): return fake.password(length=self.min_length)
[docs]class Email(Property): """Like :class:`.String` but constrained to values that are valid e-mail addresses. Examples: >>> from req.schema import Email >>> e = Email() >>> e.validate("foo@example.com") >>> e.validate("foo") Traceback (most recent call last): ... ValidationError: ['invalid e-mail "foo"'] """ EMAIL_RE = re.compile(r"^\S+@\S+$") def _validate(self, data): if not isinstance(data, six.string_types) or \ not self.EMAIL_RE.match(data): raise ValidationError("invalid e-mail {!r}".format(data)) def _generate(self): return fake.email()
[docs]class URI(Property): """Like :class:`.String` but constained to values that are valid URIs. Examples: >>> from req.schema import URI >>> u = URI() >>> u.validate("http://google.com") >>> u.validate("ftp://example.com") >>> u.validate("//example.com") Traceback (most recent call last): ... ValidationError: ['invalid URI "//example.com"'] """ URI_RE = re.compile(r"^((\S+):)+\S+$") def _validate(self, data): if not isinstance(data, six.string_types) or \ not self.URI_RE.match(data): raise ValidationError("invalid URI {!r}".format(data)) def _generate(self): return fake.uri()
[docs]class IPv4(Property): """Like :class:`.String` but constained to values that are valid IPv4 addresses. Examples: >>> from req.schema import IPv4 >>> i = IPv4() >>> i.validate("127.0.0.1") >>> i.validate("10.1.1.5") >>> i.validate("255.255.255.256") Traceback (most recent call last): ... ValidationError: ['invalid ipv4 address "255.255.255.256"'] """ def _validate(self, data): error = partial(ValidationError, "invalid ipv4 address '{}'".format(data)) if not isinstance(data, six.string_types): raise error() if data.count(".") < 3: raise error() try: socket.inet_pton(socket.AF_INET, data) except socket.error: raise error() def _generate(self): return fake.ipv4()
[docs]class IPv6(Property): """Like :class:`.IPv4` but for IPv6 addresses. Examples: >>> from req.schema import IPv6 >>> i = IPv6() >>> i.validate("::") >>> i.validate("2001:db8::1") >>> i.validate("2001:db8::z1") Traceback (most recent call last): ... ValidationError: ['invalid ipv6 address "2001:db8::z1"'] """ def _validate(self, data): try: socket.inet_pton(socket.AF_INET6, data) except (socket.error, TypeError): raise ValidationError("invalid ipv6 address '{}'".format(data)) def _generate(self): return fake.ipv6()
[docs]class DateTime(Property): """A property for date time strings. Examples: >>> from req.schema import DateTime >>> d = DateTime() >>> d.validate("2015-01-01T00:00Z") >>> d.validate("2015-01-01T00:00") Traceback (most recent call last): ... ValidationError: ['datetimes must include timezone'] >>> d = DateTime(_format="%Y") >>> d.update("2015-01-01T12:35Z") >>> d.materialize() "2015" Parameters: _format(str): The :class:`.datetime` compatible format to use when rendering this property. """ def __init__(self, _format=None, **options): super(DateTime, self).__init__(**options) self.format_ = _format def materialize(self, sparse=False): if self._value is None: return None if self.format_ is None: return self._value.isoformat() return self._value.strftime(self.format_) def parse(self, date): # XXX: `date` can be `None` on assignment when the date belongs # to a :class:`.Schema` or when :meth:`.update` is called. if date is None: return None if not isinstance(date, datetime.datetime): return parse_datetime(date) return date def update(self, date): self._value = self.validate(date) def _validate(self, date): try: date = self.parse(date) if date.tzinfo is None: raise ValidationError("datetimes must include timezone") return date except (TypeError, AttributeError, ValueError, OverflowError): raise ValidationError("invalid datetime '{}'".format(date)) def _generate(self): return fake.date_time().replace(tzinfo=dateutil.tz.tzutc()).isoformat()
class Date(Property): """A property for date strings. Examples: >>> from req.schema import Date >>> d = Date(_format="%Y") >>> d.update("2015-01-01") >>> d.materialize() "2015" Parameters: _format(str): The :class:`.date` compatible format to use when rendering this property. """ def __init__(self, _format=None, **options): super(Date, self).__init__(**options) self.format_ = _format def materialize(self, sparse=False): if self._value is None: return None if self.format_ is None: return self._value.isoformat() return self._value.strftime(self.format_) def parse(self, date): # XXX: `date` can be `None` on assignment when the date belongs # to a :class:`.Schema` or when :meth:`.update` is called. if date is None: return None if isinstance(date, datetime.date): return date if isinstance(date, datetime.datetime): return date.date() return parse_date(date) def update(self, date): self._value = self.validate(date) def _validate(self, date): try: date = self.parse(date) return date except (TypeError, AttributeError, ValueError, OverflowError): raise ValidationError("invalid date '{}'".format(date)) def _generate(self): return fake.date()
[docs]class Or(Property): r"""A property for values that can have multiple shapes. Examples: >>> from req.schema import Or, String, Integer >>> o = Or(String(), Integer()) >>> o.validate("a") >>> o.validate(1) >>> o.validate(1.5) Traceback (most recent call last): ... ValidationError: [['invalid string for String schema'], ['invalid integer for Integer schema']] Parameters: *validations(SchemaLike list): A list of acceptable types. Values are validated against each one in order until one succeeds or all of them fail. """ def __init__(self, *validations, **options): super(Or, self).__init__(**options) self.validations = validations def _validate(self, data): errors = [] for validation in self.validations: try: return validation.validate(data) except ValidationError as e: messages = e.message if not isinstance(messages, (tuple, list)): messages = [messages] validation_name = validation.__class__.__name__ messages = ["{} for {} schema".format(m, validation_name) for m in messages] errors.append(messages) else: raise ValidationError(errors) def _generate(self): return random.choice(self.validations).generate()