from __future__ import unicode_literals
from collections import Iterable
import datetime
import re
from decimal import Decimal as _D
try:
from urllib.parse import urlsplit, urlunsplit
except ImportError: # Python 2
from urlparse import urlsplit, urlunsplit
from pytz import utc
from goblin._compat import (string_types, text_type, float_types,
integer_types, array_types, bool_types, print_)
from goblin.exceptions import GoblinException, ValidationError
# These values, if given to validate(), will trigger the self.required check.
EMPTY_VALUES = (None, '', [], (), {})
[docs]class BaseValidator(object):
message = 'Enter a valid value.'
code = 'invalid'
def __init__(self, message=None, code=None):
if message is not None:
self.message = message
if code is not None:
self.code = code
def __call__(self, value):
"""
Validates that the input passes validation
"""
return value
pass_all_validator = BaseValidator()
[docs]class BooleanValidator(BaseValidator):
message = 'Enter a valid Boolean.'
def __call__(self, value):
if not isinstance(value, bool_types):
raise ValidationError(self.message, self.code)
return value
bool_validator = BooleanValidator()
[docs]class NumericValidator(BaseValidator):
message = 'Enter a valid number.'
data_types = float_types + integer_types + (_D, )
def __call__(self, value):
if not isinstance(value, self.__class__.data_types):
raise ValidationError(self.message, code=self.code)
return value
numeric_validator = NumericValidator()
[docs]class FloatValidator(NumericValidator):
data_types = float_types
float_validator = FloatValidator()
[docs]class DecimalValidator(NumericValidator):
data_types = float_types + (_D, )
decimal_validator = DecimalValidator()
[docs]class IntegerValidator(NumericValidator):
data_types = integer_types
integer_validator = IntegerValidator()
[docs]class LongValidator(NumericValidator):
data_types = integer_types
long_validator = LongValidator()
[docs]class PositiveIntegerValidator(NumericValidator):
data_types = integer_types
def __call__(self, value):
super(PositiveIntegerValidator, self).__call__(value)
if value < 0:
raise ValidationError("Value must be 0 or greater")
return value
positive_integer_validator = PositiveIntegerValidator()
[docs]class StringValidator(BaseValidator):
message = 'Enter a valid string: {}'
data_type = string_types
def __call__(self, value):
if not isinstance(value, self.data_type):
raise ValidationError(self.message.format(value), code=self.code)
return value
string_validator = StringValidator()
[docs]class ListValidator(BaseValidator):
message = 'Enter a valid list'
data_types = array_types
def __call__(self, value):
if not isinstance(value, self.data_types):
raise ValidationError(self.message, code=self.code)
return value
list_validator = ListValidator()
[docs]class DictValidator(BaseValidator):
message = 'Enter a valid dict'
def __call__(self, value):
if not isinstance(value, dict):
raise ValidationError(self.message, code=self.code)
return value
dict_validator = DictValidator()
[docs]class DateTimeValidator(BaseValidator):
message = 'Not a valid DateTime: {}'
def __call__(self, value):
if not isinstance(value, datetime.datetime) and value is not None:
raise ValidationError(self.message.format(value), code=self.code)
return value
datetime_validator = DateTimeValidator()
[docs]class DateTimeUTCValidator(BaseValidator):
message = 'Not a valid UTC DateTime: {}'
def __call__(self, value):
super(DateTimeUTCValidator, self).__call__(value)
if value is None:
return
if not isinstance(value, datetime.datetime) and value is not None:
raise ValidationError(self.message.format(value), code=self.code)
if value and value.tzinfo != utc:
# print_("Got value with timezone: {} - {}".format(value, value.tzinfo))
try:
value = value.astimezone(tz=utc)
except ValueError: # last ditch effort
try:
value = value.replace(tzinfo=utc)
except (AttributeError, TypeError):
raise ValidationError(
self.message.format(value), code=self.code)
except AttributeError: # pragma: no cover
# This should never happen, unless it isn't a datetime object
raise ValidationError(self.message % (value, ), code=self.code)
# print_("Datetime passed validation: {} - {}".format(value, value.tzinfo))
return value
datetime_utc_validator = DateTimeUTCValidator()
[docs]class RegexValidator(BaseValidator):
regex = ''
def __init__(self, regex=None, message=None, code=None):
super(RegexValidator, self).__init__(message=message, code=code)
if regex is not None:
self.regex = regex
# Compile the regex if it was not passed pre-compiled.
if isinstance(self.regex, string_types): # pragma: no cover
self.regex = re.compile(self.regex)
def __call__(self, value):
"""
Validates that the input matches the regular expression.
"""
if not self.regex.search(text_type(value)):
raise ValidationError(self.message, code=self.code)
else:
return value
[docs]class URLValidator(RegexValidator):
regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
message = 'Enter a valid URL address: {}'
code = 'invalid'
def __call__(self, value):
try:
super(URLValidator, self).__call__(value)
except ValidationError as e:
# Trivial case failed. Try for possible IDN domain
if value:
value = text_type(value)
scheme, netloc, path, query, fragment = urlsplit(value)
try:
# IDN -> ACE
netloc = netloc.encode('idna').decode('ascii')
except UnicodeError: # invalid domain part
raise ValidationError(self.message.format(value),
code=self.code)
url = urlunsplit((scheme, netloc, path, query, fragment))
return super(URLValidator, self).__call__(url)
else:
raise ValidationError(self.message.format(value),
code=self.code)
return value
[docs]class EmailValidator(RegexValidator):
regex = re.compile(
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
# quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"'
r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)$)' # domain
r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', # literal form, ipv4 address (SMTP 4.1.3)
re.IGNORECASE)
message = 'Enter a valid email address: {}'
code = 'invalid'
def __call__(self, value):
try:
super(EmailValidator, self).__call__(value)
except ValidationError as e:
# Trivial case failed. Try for possible IDN domain-part
if value and isinstance(value, string_types) and '@' in value:
parts = value.split('@')
try:
parts[-1] = parts[-1].encode('idna').decode('ascii')
except UnicodeError:
raise ValidationError(self.message.format(value),
code=self.code)
super(EmailValidator, self).__call__('@'.join(parts))
else:
raise ValidationError(self.message.format(value),
code=self.code)
return value
validate_email = EmailValidator()
slug_re = re.compile(r'^[-a-zA-Z0-9_]+$')
validate_slug = RegexValidator(
slug_re,
"Enter a valid 'slug' - letters, numbers, underscores or hyphens.",
'invalid')
## IPv4
ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
validate_ipv4_address = RegexValidator(ipv4_re, 'Enter a valid IPv4 address.', 'invalid')
# IPv6
ipv6_re = re.compile(
r'('
r'([0-9A-F]{1,4}:){7,7}[0-9A-F]{1,4}|' # 1:2:3:4:5:6:7:8
r'([0-9A-F]{1,4}:){1,7}:|' # 1:: 1:2:3:4:5:6:7::
r'([0-9A-F]{1,4}:){1,6}:[0-9A-F]{1,4}|' # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8
r'([0-9A-F]{1,4}:){1,5}(:[0-9A-F]{1,4}){1,2}|' # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8
r'([0-9A-F]{1,4}:){1,4}(:[0-9A-F]{1,4}){1,3}|' # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8
r'([0-9A-F]{1,4}:){1,3}(:[0-9A-F]{1,4}){1,4}|' # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8
r'([0-9A-F]{1,4}:){1,2}(:[0-9A-F]{1,4}){1,5}|' # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8
r'[0-9A-F]{1,4}:((:[0-9A-F]{1,4}){1,6})|' # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8
r':((:[0-9A-F]{1,4}){1,7}|:)' # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::
r')', re.IGNORECASE)
validate_ipv6_address = RegexValidator(ipv6_re, 'Enter a valid IPv6 address.',
'invalid')
# IPv6/4
ipv64_re = re.compile(
ipv6_re.pattern[:-1] + r'|' +
r'::(ffff(:0{1,4}){0,1}:){0,1}' # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255
r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}' # ... (IPv4-mapped IPv6 addresses and IPv4-translated addresses)
r'(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|'
r'([0-9a-fA-F]{1,4}:){1,4}:' # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address)
r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}'
r'(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])'
r')', re.IGNORECASE)
validate_ipv6_ipv4_address = RegexValidator(ipv64_re,
'Enter a valid IPv4, IPv6, '
'(IPv4-mapped, IPv4 Translated or IPv4-Embedded) IPv6 address',
'invalid')
validate_url = URLValidator()
re_uuid = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
validate_uuid4 = RegexValidator(re_uuid, 'Enter a valid UUID4.', 'invalid')
validate_uuid1 = RegexValidator(re_uuid, 'Enter a valid UUID1.', 'invalid')