shanbay/peeweext

View on GitHub
peeweext/validation.py

Summary

Maintainability
A
2 hrs
Test Coverage
"""Validator for peewee Model"""
import re
import functools
import ipaddress
from urllib.parse import urlsplit, urlunsplit


class ValidationError(Exception):
    pass


class BaseValidator:
    def validate(self, value):
        """Validate value.
        :param value: value to validate
        :return None
        :raise ValidationError
        """
        pass

    def __call__(self, value):
        self.validate(value)


class ExclusionValidator(BaseValidator):
    def __init__(self, *args):
        self._data = args

    def validate(self, value):
        if value in self._data:
            raise ValidationError('value {:s} is in {:s}'.format(
                str(value), str(self._data)))


class InclusionValidator(ExclusionValidator):
    def validate(self, value):
        if value not in self._data:
            raise ValidationError('value {:s} is not in {:s}'.format(
                str(value), str(self._data)))


class RegexValidator(BaseValidator):
    def __init__(self, regex):
        self._regex = regex
        self._compiled_regex = re.compile(regex)

    def validate(self, value):
        """Validate string by regex
        :param value: str
        :return:
        """
        if not self._compiled_regex.match(value):
            raise ValidationError(
                'value {:s} not match r"{:s}"'.format(value, self._regex))


class URLValidator(RegexValidator):
    """
    See https://docs.djangoproject.com/zh-hans/2.0/ref/validators/#urlvalidator
    """

    ul = '\u00a1-\uffff'  # unicode letters range (must not be a raw string)

    # IP patterns
    ipv4_re = (
        r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.'  # noqa: W605
        '(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'  # noqa: W605
    )
    ipv6_re = r'\[[0-9a-f:\.]+\]'  # (simple regex, validated later)

    # Host patterns
    hostname_re = r'[a-z' + ul + \
        r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
    # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
    domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
    tld_re = (
        r'\.'  # dot
        r'(?!-)'  # can't start with a dash
        r'(?:[a-z' + ul + '-]{2,63}'  # domain label
        r'|xn--[a-z0-9]{1,59})'  # or punycode label
        r'(?<!-)'  # can't end with a dash
        r'\.?'  # may have a trailing dot
    )
    host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'

    _compiled_regex = re.compile(
        r'^(?:[a-z0-9\.\-\+]*)://'  # scheme is validated separately
        r'(?:\S+(?::\S*)?@)?'  # user:pass authentication
        r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
        r'(?::\d{2,5})?'  # port
        r'(?:[/?#][^\s]*)?'  # resource path
        r'\Z', re.IGNORECASE)
    _regex = _compiled_regex.pattern
    message = 'Enter a valid URL.'
    schemes = ['http', 'https', 'ftp', 'ftps']

    def __init__(self, schemes=None, null=False):
        self.null = null
        if schemes is not None:
            self.schemes = schemes

    def validate(self, value):
        if self.null and not value:
            return
        # Check first if the scheme is valid
        scheme = value.split('://')[0].lower()
        if scheme not in self.schemes:
            raise ValidationError(self.message)

        # Then check full URL
        try:
            super().validate(value)
        except ValidationError as e:
            # Trivial case failed. Try for possible IDN domain
            if value:
                try:
                    scheme, netloc, path, query, fragment = urlsplit(value)
                except ValueError:  # for example, "Invalid IPv6 URL"
                    raise ValidationError(self.message)
                try:
                    netloc = netloc.encode('idna').decode(
                        'ascii')  # IDN -> ACE
                except UnicodeError:  # invalid domain part
                    raise e
                url = urlunsplit((scheme, netloc, path, query, fragment))
                super().validate(url)
            else:
                raise
        else:
            # Now verify IPv6 in the netloc part
            host_match = re.search(
                r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
            if host_match:
                potential_ip = host_match.groups()[0]
                try:
                    ipaddress.IPv6Address(potential_ip)
                except ValueError:
                    raise ValidationError(self.message)

        # The maximum length of a full host name is 253 characters per RFC 1034
        # section 3.1. It's defined to be 255 bytes or less, but this includes
        # one byte for the length of the name and one byte for the trailing dot
        # that's used to indicate absolute names in DNS.
        if len(urlsplit(value).netloc) > 253:
            raise ValidationError(self.message)


class LengthValidator(BaseValidator):
    def __init__(self, min_length, max_length):
        self.min_length = min_length
        self.max_length = max_length

    def validate(self, value):
        length = len(value)
        if self.min_length <= length <= self.max_length:
            pass
        else:
            raise ValidationError(
                'length {:d} not in range ({:d},{:d})'.format(
                    length, self.min_length, self.max_length))


class validates:
    """A decorator to store validators.

    :param args: Validator objects
    """

    def __init__(self, *args):
        self.validators = list(args)

    def __call__(self, func):
        for validator in self.validators:
            validator.func_name = func.__name__

        @functools.wraps(func)
        def wrapper(instance, value):
            for validator in self.validators:
                validator(value)
            return func(instance, value)

        return wrapper