# -*- coding: utf-8 -*-
# The lack of a module docstring for this module is **INTENTIONAL**.
# The module is imported into the documentation using Sphinx's autodoc
# extension, and its member function documentation is automatically incorporated
# there as needed.
import decimal as decimal_
import fractions
import io
import math
import os
import uuid as uuid_
import datetime as datetime_
import string as string_
import sys
from ast import parse
import jsonschema
from validator_collection._compat import numeric_types, integer_types, datetime_types,\
date_types, time_types, timestamp_types, tzinfo_types, POSITIVE_INFINITY, \
NEGATIVE_INFINITY, TimeZone, json_, is_py2, is_py3, dict_, float_, basestring, re
from validator_collection._decorators import disable_on_env
from validator_collection import errors
URL_UNSAFE_CHARACTERS = ('[', ']', '{', '}', '|', '^', '%', '~')
URL_REGEX = re.compile(
r"^"
# protocol identifier
r"(?:(?:https?|ftp)://)"
# user:pass authentication
r"(?:\S+(?::\S*)?@)?"
r"(?:"
# IP address exclusion
# private & local networks
r"(?!(?:10|127)(?:\.\d{1,3}){3})"
r"(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})"
r"(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})"
# IP address dotted notation octets
# excludes loopback network 0.0.0.0
# excludes reserved space >= 224.0.0.0
# excludes network & broadcast addresses
# (first & last IP address of each class)
r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])"
r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}"
r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))"
r"|"
r"(?:"
r"(?:localhost|invalid|test|example)|("
# host name
r"(?:(?:[A-z\u00a1-\uffff0-9]-*_*)*[A-z\u00a1-\uffff0-9]+)"
# domain name
r"(?:\.(?:[A-z\u00a1-\uffff0-9]-*)*[A-z\u00a1-\uffff0-9]+)*"
# TLD identifier
r"(?:\.(?:[A-z\u00a1-\uffff]{2,}))"
r")))"
# port number
r"(?::\d{2,5})?"
# resource path
r"(?:/\S*)?"
r"$"
, re.UNICODE)
URL_SPECIAL_IP_REGEX = re.compile(
r"^"
# protocol identifier
r"(?:(?:https?|ftp)://)"
# user:pass authentication
r"(?:\S+(?::\S*)?@)?"
r"(?:"
# IP address dotted notation octets
# excludes loopback network 0.0.0.0
# excludes reserved space >= 224.0.0.0
# excludes network & broadcast addresses
# (first & last IP address of each class)
r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])"
r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}"
r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))"
r"|"
# host name
r"(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)"
# domain name
r"(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*"
# TLD identifier
r"(?:\.(?:[a-z\u00a1-\uffff]{2,}))"
r")"
# port number
r"(?::\d{2,5})?"
# resource path
r"(?:/\S*)?"
r"$"
, re.UNICODE)
DOMAIN_REGEX = re.compile(
r"\b((?=[a-z\u00a1-\uffff0-9-]{1,63}\.)(xn--)?[a-z\u00a1-\uffff0-9]+"
r"(-[a-z\u00a1-\uffff0-9]+)*\.)+[a-z]{2,63}\b",
re.UNICODE|re.IGNORECASE
)
URL_PROTOCOLS = ('http://',
'https://',
'ftp://')
SPECIAL_USE_DOMAIN_NAMES = ('localhost',
'invalid',
'test',
'example')
EMAIL_REGEX = re.compile(
r"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\""
r"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*\")"
r"@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])"
r"?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}"
r"(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:"
r"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"
)
VARIABLE_NAME_REGEX = re.compile(
r"(^[a-zA-Z_])([a-zA-Z0-9_]*)"
)
MAC_ADDRESS_REGEX = re.compile(r'^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$')
IPV6_REGEX = re.compile(
'^(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)(?:%25(?:[A-Za-z0-9\\-._~]|%[0-9A-Fa-f]{2})+)?$'
)
TIMEDELTA_REGEX = re.compile(r'((?P<days>\d+) days?, )?(?P<hours>\d+):'
r'(?P<minutes>\d+):(?P<seconds>\d+(\.\d+)?)')
MIME_TYPE_REGEX = re.compile(r"^multipart|[-\w.]+/[-\w.\+]+$")
# pylint: disable=W0613
## CORE
[docs]@disable_on_env
def uuid(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid :class:`UUID <python:uuid.UUID>`.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` coerced to a :class:`UUID <python:uuid.UUID>` object /
:obj:`None <python:None>`
:rtype: :class:`UUID <python:uuid.UUID>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to a
:class:`UUID <python:uuid.UUID>`
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if isinstance(value, uuid_.UUID):
return value
try:
value = uuid_.UUID(value)
except ValueError:
raise errors.CannotCoerceError('value (%s) cannot be coerced to a valid UUID')
return value
[docs]@disable_on_env
def string(value,
allow_empty = False,
coerce_value = False,
minimum_length = None,
maximum_length = None,
whitespace_padding = False,
**kwargs):
"""Validate that ``value`` is a valid string.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param coerce_value: If ``True``, will attempt to coerce ``value`` to a string if
it is not already. If ``False``, will raise a :class:`ValueError` if ``value``
is not a string. Defaults to ``False``.
:type coerce_value: :class:`bool <python:bool>`
:param minimum_length: If supplied, indicates the minimum number of characters
needed to be valid.
:type minimum_length: :class:`int <python:int>`
:param maximum_length: If supplied, indicates the minimum number of characters
needed to be valid.
:type maximum_length: :class:`int <python:int>`
:param whitespace_padding: If ``True`` and the value is below the
``minimum_length``, pad the value with spaces. Defaults to ``False``.
:type whitespace_padding: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` is not a valid string and ``coerce_value``
is ``False``
:raises MinimumLengthError: if ``minimum_length`` is supplied and the length of
``value`` is less than ``minimum_length`` and ``whitespace_padding`` is
``False``
:raises MaximumLengthError: if ``maximum_length`` is supplied and the length of
``value`` is more than the ``maximum_length``
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
minimum_length = integer(minimum_length, allow_empty = True)
maximum_length = integer(maximum_length, allow_empty = True)
if coerce_value:
value = str(value)
elif not isinstance(value, basestring):
raise errors.CannotCoerceError('value (%s) was not coerced to a string' % value)
if value and maximum_length and len(value) > maximum_length:
raise errors.MaximumLengthError(
'value (%s) exceeds maximum length %s' % (value, maximum_length)
)
if value and minimum_length and len(value) < minimum_length:
if whitespace_padding:
value = value.ljust(minimum_length, ' ')
else:
raise errors.MinimumLengthError(
'value (%s) is below the minimum length %s' % (value, minimum_length)
)
return value
[docs]@disable_on_env
def iterable(value,
allow_empty = False,
forbid_literals = (str, bytes),
minimum_length = None,
maximum_length = None,
**kwargs):
"""Validate that ``value`` is a valid iterable.
.. hint::
This validator checks to ensure that ``value`` supports iteration using
any of Python's three iteration protocols: the ``__getitem__`` protocol,
the ``__iter__`` / ``next()`` protocol, or the inheritance from Python's
`Iterable` abstract base class.
If ``value`` supports any of these three iteration protocols, it will be
validated. However, if iteration across ``value`` raises an unsupported
exception, this function will raise an
:exc:`IterationFailedError <validator_collection.errors.IterationFailedError>`
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param forbid_literals: A collection of literals that will be considered invalid
even if they are (actually) iterable. Defaults to :class:`str <python:str>` and
:class:`bytes <python:bytes>`.
:type forbid_literals: iterable
:param minimum_length: If supplied, indicates the minimum number of members
needed to be valid.
:type minimum_length: :class:`int <python:int>`
:param maximum_length: If supplied, indicates the minimum number of members
needed to be valid.
:type maximum_length: :class:`int <python:int>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: iterable / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises NotAnIterableError: if ``value`` is not a valid iterable or
:obj:`None <python:None>`
:raises IterationFailedError: if ``value`` is a valid iterable, but iteration
fails for some unexpected exception
:raises MinimumLengthError: if ``minimum_length`` is supplied and the length of
``value`` is less than ``minimum_length`` and ``whitespace_padding`` is
``False``
:raises MaximumLengthError: if ``maximum_length`` is supplied and the length of
``value`` is more than the ``maximum_length``
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif value is None:
return None
minimum_length = integer(minimum_length, allow_empty = True, force_run = True) # pylint: disable=E1123
maximum_length = integer(maximum_length, allow_empty = True, force_run = True) # pylint: disable=E1123
if isinstance(value, forbid_literals):
raise errors.NotAnIterableError('value type (%s) not iterable' % type(value))
try:
iter(value)
except TypeError:
raise errors.NotAnIterableError('value type (%s) not iterable' % type(value))
except Exception as error:
raise errors.IterationFailedError('iterating across value raised an unexpected Exception: "%s"' % error)
if value and minimum_length is not None and len(value) < minimum_length:
raise errors.MinimumLengthError(
'value has fewer items than the minimum length %s' % minimum_length
)
if value and maximum_length is not None and len(value) > maximum_length:
raise errors.MaximumLengthError(
'value has more items than the maximum length %s' % maximum_length
)
return value
[docs]@disable_on_env
def none(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is :obj:`None <python:None>`.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
is empty but **not** :obj:`None <python:None>`. If ``False``, raises a
:class:`NotNoneError` if ``value`` is empty but **not**
:obj:`None <python:None>`. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: :obj:`None <python:None>`
:raises NotNoneError: if ``allow_empty`` is ``False`` and ``value`` is empty
but **not** :obj:`None <python:None>` and
"""
if value is not None and not value and allow_empty:
pass
elif (value is not None and not value) or value:
raise errors.NotNoneError('value was not None')
return None
[docs]@disable_on_env
def not_empty(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is not empty.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
"""
if not value and allow_empty:
return None
elif not value:
raise errors.EmptyValueError('value was empty')
return value
[docs]@disable_on_env
def variable_name(value,
allow_empty = False,
**kwargs):
"""Validate that the value is a valid Python variable name.
.. caution::
This function does **NOT** check whether the variable exists. It only
checks that the ``value`` would work as a Python variable (or class, or
function, etc.) name.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
is_valid = VARIABLE_NAME_REGEX.fullmatch(value)
if not is_valid:
raise errors.InvalidVariableNameError(
'value (%s) is not a valid variable name' % value
)
try:
parse('%s = None' % value)
except (SyntaxError, ValueError, TypeError):
raise errors.InvalidVariableNameError(
'value (%s) is not a valid variable name' % value
)
return value
[docs]@disable_on_env
def dict(value,
allow_empty = False,
json_serializer = None,
**kwargs):
"""Validate that ``value`` is a :class:`dict <python:dict>`.
.. hint::
If ``value`` is a string, this validator will assume it is a JSON
object and try to convert it into a :class:`dict <python:dict>`
You can override the JSON serializer used by passing it to the
``json_serializer`` property. By default, will utilize the Python
:class:`json <json>` encoder/decoder.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param json_serializer: The JSON encoder/decoder to use to deserialize a
string passed in ``value``. If not supplied, will default to the Python
:class:`json <python:json>` encoder/decoder.
:type json_serializer: callable
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`dict <python:dict>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to a
:class:`dict <python:dict>`
:raises NotADictError: if ``value`` is not a :class:`dict <python:dict>`
"""
original_value = value
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if json_serializer is None:
json_serializer = json_
if isinstance(value, str):
try:
value = json_serializer.loads(value)
except Exception:
raise errors.CannotCoerceError(
'value (%s) cannot be coerced to a dict' % original_value
)
value = dict(value,
json_serializer = json_serializer)
if not isinstance(value, dict_):
raise errors.NotADictError('value (%s) is not a dict' % original_value)
return value
[docs]@disable_on_env
def json(value,
schema = None,
allow_empty = False,
json_serializer = None,
**kwargs):
"""Validate that ``value`` conforms to the supplied JSON Schema.
.. note::
``schema`` supports JSON Schema Drafts 3 - 7. Unless the JSON Schema indicates the
meta-schema using a ``$schema`` property, the schema will be assumed to conform to
Draft 7.
.. hint::
If either ``value`` or ``schema`` is a string, this validator will assume it is a
JSON object and try to convert it into a :class:`dict <python:dict>`.
You can override the JSON serializer used by passing it to the
``json_serializer`` property. By default, will utilize the Python
:class:`json <json>` encoder/decoder.
:param value: The value to validate.
:param schema: An optional JSON Schema against which ``value`` will be validated.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param json_serializer: The JSON encoder/decoder to use to deserialize a
string passed in ``value``. If not supplied, will default to the Python
:class:`json <python:json>` encoder/decoder.
:type json_serializer: callable
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`dict <python:dict>` / :class:`list <python:list>` of
:class:`dict <python:dict>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to a
:class:`dict <python:dict>`
:raises NotJSONError: if ``value`` cannot be deserialized from JSON
:raises NotJSONSchemaError: if ``schema`` is not a valid JSON Schema object
:raises JSONValidationError: if ``value`` does not validate against the JSON Schema
"""
original_value = value
original_schema = schema
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not json_serializer:
json_serializer = json_
if isinstance(value, str):
try:
value = json_serializer.loads(value)
except Exception:
raise errors.CannotCoerceError(
'value (%s) cannot be deserialized from JSON' % original_value
)
if isinstance(schema, str):
try:
schema = dict(schema,
allow_empty = allow_empty,
json_serializer = json_serializer,
**kwargs)
except Exception:
raise errors.CannotCoerceError(
'schema (%s) cannot be coerced to a dict' % original_schema
)
if not isinstance(value, (list, dict_)):
raise errors.NotJSONError('value (%s) is not a JSON object' % original_value)
if original_schema and not isinstance(schema, dict_):
raise errors.NotJSONError('schema (%s) is not a JSON object' % original_schema)
if not schema:
return value
try:
jsonschema.validate(value, schema)
except jsonschema.exceptions.ValidationError as error:
raise errors.JSONValidationError(error.message)
except jsonschema.exceptions.SchemaError as error:
raise errors.NotJSONSchemaError(error.message)
return value
## DATE / TIME
[docs]@disable_on_env
def date(value,
allow_empty = False,
minimum = None,
maximum = None,
coerce_value = True,
**kwargs):
"""Validate that ``value`` is a valid date.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :class:`datetime <python:datetime.datetime>`
/ :class:`date <python:datetime.date>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param minimum: If supplied, will make sure that ``value`` is on or after this value.
:type minimum: :class:`datetime <python:datetime.datetime>` /
:class:`date <python:datetime.date>` / compliant :class:`str <python:str>`
/ :obj:`None <python:None>`
:param maximum: If supplied, will make sure that ``value`` is on or before this
value.
:type maximum: :class:`datetime <python:datetime.datetime>` /
:class:`date <python:datetime.date>` / compliant :class:`str <python:str>`
/ :obj:`None <python:None>`
:param coerce_value: If ``True``, will attempt to coerce ``value`` to a
:class:`date <python:datetime.date>` if it is a timestamp value. If ``False``,
will not.
:type coerce_value: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`date <python:datetime.date>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to a
:class:`date <python:datetime.date>` and is not :obj:`None <python:None>`
:raises MinimumValueError: if ``minimum`` is supplied but ``value`` occurs before
``minimum``
:raises MaximumValueError: if ``maximum`` is supplied but ``value`` occurs after
``maximum``
"""
# pylint: disable=too-many-branches
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
minimum = date(minimum, allow_empty = True, force_run = True) # pylint: disable=E1123
maximum = date(maximum, allow_empty = True, force_run = True) # pylint: disable=E1123
if not isinstance(value, date_types):
raise errors.CannotCoerceError(
'value (%s) must be a date object, datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value, type(value))
)
elif isinstance(value, datetime_.datetime) and not coerce_value:
raise errors.CannotCoerceError(
'value (%s) must be a date object, or '
'ISO 8601-formatted string, '
'but was %s' % (value, type(value))
)
elif isinstance(value, datetime_.datetime) and coerce_value:
value = value.date()
elif isinstance(value, timestamp_types) and coerce_value:
try:
value = datetime_.date.fromtimestamp(value)
except ValueError:
raise errors.CannotCoerceError(
'value (%s) must be a date object, datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value, type(value))
)
elif isinstance(value, str):
try:
value = datetime_.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
if coerce_value:
value = value.date()
else:
raise errors.CannotCoerceError(
'value (%s) must be a date object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value, type(value))
)
except ValueError:
if len(value) > 10 and not coerce_value:
raise errors.CannotCoerceError(
'value (%s) must be a date object, or '
'ISO 8601-formatted string, '
'but was %s' % (value, type(value))
)
if ' ' in value:
value = value.split(' ')[0]
if 'T' in value:
value = value.split('T')[0]
if len(value) != 10:
raise errors.CannotCoerceError(
'value (%s) must be a date object, datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value, type(value))
)
try:
year = int(value[:4])
month = int(value[5:7])
day = int(value[-2:])
value = datetime_.date(year, month, day)
except (ValueError, TypeError):
raise errors.CannotCoerceError(
'value (%s) must be a date object, datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value, type(value))
)
elif isinstance(value, numeric_types) and not coerce_value:
raise errors.CannotCoerceError(
'value (%s) must be a date object, or '
'ISO 8601-formatted string, '
'but was %s' % (value, type(value))
)
if minimum and value and value < minimum:
raise errors.MinimumValueError(
'value (%s) is before the minimum given (%s)' % (value.isoformat(),
minimum.isoformat())
)
if maximum and value and value > maximum:
raise errors.MaximumValueError(
'value (%s) is after the maximum given (%s)' % (value.isoformat(),
maximum.isoformat())
)
return value
[docs]@disable_on_env
def datetime(value,
allow_empty = False,
minimum = None,
maximum = None,
coerce_value = True,
**kwargs):
"""Validate that ``value`` is a valid datetime.
.. caution::
If supplying a string, the string needs to be in an ISO 8601-format to pass
validation. If it is not in an ISO 8601-format, validation will fail.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :class:`datetime <python:datetime.datetime>`
/ :class:`date <python:datetime.date>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param minimum: If supplied, will make sure that ``value`` is on or after this value.
:type minimum: :class:`datetime <python:datetime.datetime>` /
:class:`date <python:datetime.date>` / compliant :class:`str <python:str>` /
:obj:`None <python:None>`
:param maximum: If supplied, will make sure that ``value`` is on or before this
value.
:type maximum: :class:`datetime <python:datetime.datetime>` /
:class:`date <python:datetime.date>` / compliant :class:`str <python:str>` /
:obj:`None <python:None>`
:param coerce_value: If ``True``, will coerce dates to
:class:`datetime <python:datetime.datetime>` objects with times of 00:00:00. If ``False``, will error
if ``value`` is not an unambiguous timestamp. Defaults to ``True``.
:type coerce_value: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`datetime <python:datetime.datetime>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to a
:class:`datetime <python:datetime.datetime>` value and is not
:obj:`None <python:None>`
:raises MinimumValueError: if ``minimum`` is supplied but ``value`` occurs
before ``minimum``
:raises MaximumValueError: if ``maximum`` is supplied but ``value`` occurs
after ``minimum``
"""
# pylint: disable=too-many-branches
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
minimum = datetime(minimum, allow_empty = True, force_run = True) # pylint: disable=E1123
maximum = datetime(maximum, allow_empty = True, force_run = True) # pylint: disable=E1123
if not isinstance(value, datetime_types):
raise errors.CannotCoerceError(
'value (%s) must be a date object, datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value,
type(value))
)
elif isinstance(value, timestamp_types) and coerce_value:
try:
value = datetime_.datetime.fromtimestamp(value)
except ValueError:
raise errors.CannotCoerceError(
'value (%s) must be a date object, datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value,
type(value))
)
elif isinstance(value, str):
# pylint: disable=line-too-long
try:
if 'T' in value:
value = datetime_.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f%z')
else:
value = datetime_.datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f%z')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value, '%Y/%m/%dT%H:%M:%S%z')
else:
value = datetime_.datetime.strptime(value, '%Y/%m/%d %H:%M:%S%z')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z')
else:
value = datetime_.datetime.strptime(value, '%Y-%m-%d %H:%M:%S%z')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value,
'%Y/%m/%dT%H:%M:%S%z')
else:
value = datetime_.datetime.strptime(value,
'%Y/%m/%d %H:%M:%S%z')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
else:
value = datetime_.datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value, '%Y/%m/%dT%H:%M:%S')
else:
value = datetime_.datetime.strptime(value, '%Y/%m/%d %H:%M:%S')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')
else:
value = datetime_.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
if 'T' in value:
value = datetime_.datetime.strptime(value,
'%Y/%m/%dT%H:%M:%S')
else:
value = datetime_.datetime.strptime(value,
'%Y/%m/%d %H:%M:%S')
except ValueError:
if coerce_value:
value = date(value)
else:
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp' % value
)
# pylint: enable=line-too-long
elif isinstance(value, numeric_types) and not coerce_value:
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp' % value
)
if isinstance(value, datetime_.date) and not isinstance(value, datetime_.datetime):
if coerce_value:
value = datetime_.datetime(value.year, # pylint: disable=R0204
value.month,
value.day,
0,
0,
0,
0)
else:
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp' % value
)
if minimum and value and value < minimum:
raise errors.MinimumValueError(
'value (%s) is before the minimum given (%s)' % (value.isoformat(),
minimum.isoformat())
)
if maximum and value and value > maximum:
raise errors.MaximumValueError(
'value (%s) is after the maximum given (%s)' % (value.isoformat(),
maximum.isoformat())
)
return value
[docs]@disable_on_env
def time(value,
allow_empty = False,
minimum = None,
maximum = None,
coerce_value = True,
**kwargs):
"""Validate that ``value`` is a valid :class:`time <python:datetime.time>`.
.. caution::
This validator will **always** return the time as timezone naive (effectively
UTC). If ``value`` has a timezone / UTC offset applied, the validator will
coerce the value returned back to UTC.
:param value: The value to validate.
:type value: :func:`datetime <validator_collection.validators.datetime>` or
:func:`time <validator_collection.validators.time>`-compliant
:class:`str <python:str>` / :class:`datetime <python:datetime.datetime>` /
:class:`time <python:datetime.time> / numeric / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param minimum: If supplied, will make sure that ``value`` is on or after this value.
:type minimum: :func:`datetime <validator_collection.validators.datetime>` or
:func:`time <validator_collection.validators.time>`-compliant
:class:`str <python:str>` / :class:`datetime <python:datetime.datetime>` /
:class:`time <python:datetime.time> / numeric / :obj:`None <python:None>`
:param maximum: If supplied, will make sure that ``value`` is on or before this
value.
:type maximum: :func:`datetime <validator_collection.validators.datetime>` or
:func:`time <validator_collection.validators.time>`-compliant
:class:`str <python:str>` / :class:`datetime <python:datetime.datetime>` /
:class:`time <python:datetime.time> / numeric / :obj:`None <python:None>`
:param coerce_value: If ``True``, will attempt to coerce/extract a
:class:`time <python:datetime.time>` from ``value``. If ``False``, will only
respect direct representations of time. Defaults to ``True``.
:type coerce_value: :class:`bool <python:bool>`
:returns: ``value`` in UTC time / :obj:`None <python:None>`
:rtype: :class:`time <python:datetime.time>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to a
:class:`time <python:datetime.time>` and is not :obj:`None <python:None>`
:raises MinimumValueError: if ``minimum`` is supplied but ``value`` occurs
before ``minimum``
:raises MaximumValueError: if ``maximum`` is supplied but ``value`` occurs
after ``minimum``
"""
# pylint: disable=too-many-branches
if not value and not allow_empty:
if isinstance(value, datetime_.time):
pass
else:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
if not isinstance(value, datetime_.time):
return None
minimum = time(minimum, allow_empty = True, force_run = True) # pylint: disable=E1123
maximum = time(maximum, allow_empty = True, force_run = True) # pylint: disable=E1123
if not isinstance(value, time_types):
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value,
type(value))
)
elif isinstance(value, datetime_.datetime) and not coerce_value:
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value,
type(value))
)
elif isinstance(value, datetime_.datetime) and coerce_value:
value = value.time()
elif isinstance(value, timestamp_types):
try:
datetime_value = datetime(value, force_run = True) # pylint: disable=E1123
if coerce_value:
value = datetime_value.time()
else:
raise errors.CannotCoerceError(
'value (%s) must be a time object, '
'ISO 8601-formatted string, '
'but was %s' % (value,
type(value))
)
except ValueError:
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value,
type(value))
)
elif isinstance(value, basestring):
is_value_calculated = False
if len(value) > 10:
try:
datetime_value = datetime(value, force_run = True) # pylint: disable=E1123
if coerce_value:
value = datetime_value.time()
else:
raise errors.CannotCoerceError(
'value (%s) must be a time object, '
'ISO 8601-formatted string, '
'but was %s' % (value,
type(value))
)
is_value_calculated = True
except ValueError:
pass
if not is_value_calculated:
try:
if '+' in value:
components = value.split('+')
is_offset_positive = True
elif '-' in value:
components = value.split('-')
is_offset_positive = False
else:
raise ValueError()
time_string = components[0]
if len(components) > 1:
utc_offset = components[1]
else:
utc_offset = None
time_components = time_string.split(':')
hour = int(time_components[0])
minutes = int(time_components[1])
seconds = time_components[2]
if '.' in seconds:
second_components = seconds.split('.')
seconds = int(second_components[0])
microseconds = int(second_components[1])
else:
microseconds = 0
utc_offset = timezone(utc_offset, # pylint: disable=E1123
allow_empty = True,
positive = is_offset_positive,
force_run = True)
value = datetime_.time(hour = hour,
minute = minutes,
second = seconds,
microsecond = microseconds,
tzinfo = utc_offset)
except (ValueError, TypeError, IndexError):
raise errors.CannotCoerceError(
'value (%s) must be a datetime object, '
'ISO 8601-formatted string, '
'or POSIX timestamp, but was %s' % (value,
type(value))
)
if value is not None:
value = value.replace(tzinfo = None)
if minimum is not None and value and value < minimum:
raise errors.MinimumValueError(
'value (%s) is before the minimum given (%s)' % (value.isoformat(),
minimum.isoformat())
)
if maximum is not None and value and value > maximum:
raise errors.MaximumValueError(
'value (%s) is after the maximum given (%s)' % (value.isoformat(),
maximum.isoformat())
)
return value
[docs]@disable_on_env
def timezone(value,
allow_empty = False,
positive = True,
**kwargs):
"""Validate that ``value`` is a valid :class:`tzinfo <python:datetime.tzinfo>`.
.. caution::
This does **not** verify whether the value is a timezone that actually
exists, nor can it resolve timezone names (e.g. ``'Eastern'`` or ``'CET'``).
For that kind of functionality, we recommend you utilize:
`pytz <https://pypi.python.org/pypi/pytz>`_
:param value: The value to validate.
:type value: :class:`str <python:str>` / :class:`tzinfo <python:datetime.tzinfo>`
/ numeric / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param positive: Indicates whether the ``value`` is positive or negative
(only has meaning if ``value`` is a string). Defaults to ``True``.
:type positive: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`tzinfo <python:datetime.tzinfo>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to
:class:`tzinfo <python:datetime.tzinfo>` and is not :obj:`None <python:None>`
:raises PositiveOffsetMismatchError: if ``positive`` is ``True``, but the offset
indicated by ``value`` is actually negative
:raises NegativeOffsetMismatchError: if ``positive`` is ``False``, but the offset
indicated by ``value`` is actually positive
"""
# pylint: disable=too-many-branches
original_value = value
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, tzinfo_types):
raise errors.CannotCoerceError(
'value (%s) must be a tzinfo, '
'UTC offset in seconds expressed as a number, '
'UTC offset expressed as string of form +HH:MM, '
'but was %s' % (value, type(value))
)
elif isinstance(value, datetime_.datetime):
value = value.tzinfo
elif isinstance(value, datetime_.date):
return None
elif isinstance(value, datetime_.time):
return value.tzinfo
elif isinstance(value, timestamp_types):
return None
elif isinstance(value, str):
if '+' not in value and '-' not in value:
try:
datetime_value = datetime(value, force_run = True) # pylint: disable=E1123
return datetime_value.tzinfo
except TypeError:
raise errors.CannotCoerceError(
'value (%s) must be a tzinfo, '
'UTC offset in seconds expressed as a number, '
'UTC offset expressed as string of form +HH:MM, '
'but was %s' % (value, type(value))
)
elif '-' in value:
try:
datetime_value = datetime(value, force_run = True) # pylint: disable=E1123
return datetime_value.tzinfo
except TypeError:
pass
if '+' in value and not positive:
raise errors.NegativeOffsetMismatchError(
'expected a negative UTC offset but value is positive'
)
elif '-' in value and positive and len(value) == 6:
positive = False
elif '-' in value and positive:
raise errors.PositiveOffsetMismatchError(
'expected a positive UTC offset but value is negative'
)
if '+' in value:
value = value[value.find('+'):]
elif '-' in value:
value = value[value.rfind('-'):]
value = value[1:]
offset_components = value.split(':')
if len(offset_components) != 2:
raise errors.CannotCoerceError(
'value (%s) must be a tzinfo, '
'UTC offset in seconds expressed as a number, '
'UTC offset expressed as string of form +HH:MM, '
'but was %s' % (value, type(value))
)
hour = int(offset_components[0])
minutes = int(offset_components[1])
value = (hour * 60 * 60) + (minutes * 60)
if not positive:
value = 0 - value
if isinstance(value, numeric_types):
if value > 0:
positive = True
elif value < 0:
positive = False
elif value == 0:
return None
offset = datetime_.timedelta(seconds = value)
if is_py2:
value = TimeZone(offset = offset)
elif is_py3:
try:
value = TimeZone(offset)
except ValueError:
raise errors.UTCOffsetError(
'value (%s) cannot exceed +/- 24h' % original_value
)
else:
raise NotImplementedError()
return value
[docs]@disable_on_env
def timedelta(value,
allow_empty = False,
resolution = 'seconds',
**kwargs):
"""Validate that ``value`` is a valid :class:`timedelta <python:datetime.timedelta>`.
.. note::
Expects to receive a value that is either a
:class:`timedelta <python:datetime.timedelta>`, a numeric value that can
be coerced to a :class:`timedelta <python:datetime.timedelta>`, or a
string that can be coerced to a :class:`timedelta <python:datetime.timedelta>`.
Coerceable string formats are:
* HH:MM:SS
* X day, HH:MM:SS
* X days, HH:MM:SS
* HH:MM:SS.us
* X day, HH:MM:SS.us
* X days, HH:MM:SS.us
where "us" refer to microseconds. Shout out to Alex Pitchford for sharing the
`string-parsing regex <http://kbyanc.blogspot.com/2007/08/python-reconstructing-timedeltas-from.html?showComment=1452111163905#c3907051065256615667>`_.
:param value: The value to validate. Accepts either a numeric value indicating
a number of seconds or a string indicating an amount of time.
:type value: :class:`str <python:str>` / :class:`timedelta <python:datetime.timedelta>`
/ numeric / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param resolution: Indicates the time period resolution represented by ``value``.
Accepts ``'years'``, ``'weeks'``, ``'days'``, ``'hours'``, ``'minutes'``,
``'seconds'``, ``'milliseconds'``, or ``'microseconds'``. Defaults to
``'seconds'``.
:type resolution: :class:`str <python:str>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`timedelta <python:datetime.timedelta>` / :obj:`None <python:None>`
:raises ValueError: if ``resolution`` is not a valid time period resolution
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` cannot be coerced to
:class:`timedelta <python:datetime.timedelta>` and is not :obj:`None <python:None>`
"""
# pylint: disable=too-many-branches
if isinstance(value, datetime_.timedelta):
return value
if not resolution:
resolution = 'seconds'
if resolution not in ['years',
'weeks',
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
'microseconds']:
raise ValueError('resolution (%s) not a valid time period resolution' % resolution)
timedelta_properties = {}
try:
value = numeric(value,
allow_empty = allow_empty,
force_run = True)
if resolution == 'years':
resolution = 'days'
value = value * 365
elif resolution == 'weeks':
resolution = 'days'
value = value * 7
timedelta_properties[resolution] = value
return datetime_.timedelta(**timedelta_properties)
except errors.CannotCoerceError:
try:
value = string(value,
allow_empty = allow_empty,
coerce_value = False,
force_run = True)
except errors.CannotCoerceError:
raise errors.CannotCoerceError('value (%s) could not be coerced to a'
' timedelta' % value)
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = value.lower().strip()
is_valid = TIMEDELTA_REGEX.match(value)
if not is_valid:
raise errors.CannotCoerceError('value (%s) could not be coerced to'
' a timedelta' % value)
timedelta_properties = is_valid.groupdict(0)
for key, sub_value in timedelta_properties.items():
try:
timedelta_properties[key] = numeric(sub_value,
allow_empty = True,
force_run = True)
except errors.CannotCoerceError:
raise errors.CannotCoerceError('value (%s) could not be coerced to a'
' timedelta' % value)
return datetime_.timedelta(**timedelta_properties)
## NUMBERS
[docs]@disable_on_env
def numeric(value,
allow_empty = False,
minimum = None,
maximum = None,
**kwargs):
"""Validate that ``value`` is a numeric value.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
is :obj:`None <python:None>`. If ``False``, raises an
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is :obj:`None <python:None>`.
Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param minimum: If supplied, will make sure that ``value`` is greater than or
equal to this value.
:type minimum: numeric
:param maximum: If supplied, will make sure that ``value`` is less than or
equal to this value.
:type maximum: numeric
:returns: ``value`` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is :obj:`None <python:None>` and
``allow_empty`` is ``False``
:raises MinimumValueError: if ``minimum`` is supplied and ``value`` is less
than the ``minimum``
:raises MaximumValueError: if ``maximum`` is supplied and ``value`` is more
than the ``maximum``
:raises CannotCoerceError: if ``value`` cannot be coerced to a numeric form
"""
if maximum is None:
maximum = POSITIVE_INFINITY
else:
maximum = numeric(maximum)
if minimum is None:
minimum = NEGATIVE_INFINITY
else:
minimum = numeric(minimum)
if value is None and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif value is not None:
if isinstance(value, str):
try:
value = float_(value)
except (ValueError, TypeError):
raise errors.CannotCoerceError(
'value (%s) cannot be coerced to a numeric form' % value
)
elif not isinstance(value, numeric_types):
raise errors.CannotCoerceError(
'value (%s) is not a numeric type, was %s' % (value,
type(value))
)
if value is not None and value > maximum:
raise errors.MaximumValueError(
'value (%s) exceeds maximum (%s)' % (value, maximum)
)
if value is not None and value < minimum:
raise errors.MinimumValueError(
'value (%s) less than minimum (%s)' % (value, minimum)
)
return value
[docs]@disable_on_env
def integer(value,
allow_empty = False,
coerce_value = False,
minimum = None,
maximum = None,
base = 10,
**kwargs):
"""Validate that ``value`` is an :class:`int <python:int>`.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is :obj:`None <python:None>`. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is :obj:`None <python:None>`.
Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param coerce_value: If ``True``, will force any numeric ``value`` to an integer
(always rounding up). If ``False``, will raise an error if ``value`` is numeric
but not a whole number. Defaults to ``False``.
:type coerce_value: :class:`bool <python:bool>`
:param minimum: If supplied, will make sure that ``value`` is greater than or
equal to this value.
:type minimum: numeric
:param maximum: If supplied, will make sure that ``value`` is less than or
equal to this value.
:type maximum: numeric
:param base: Indicates the base that is used to determine the integer value.
The allowed values are 0 and 2–36. Base-2, -8, and -16 literals can be
optionally prefixed with ``0b/0B``, ``0o/0O/0``, or ``0x/0X``, as with
integer literals in code. Base 0 means to interpret the string exactly as
an integer literal, so that the actual base is 2, 8, 10, or 16. Defaults to
``10``.
:returns: ``value`` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is :obj:`None <python:None>` and
``allow_empty`` is ``False``
:raises MinimumValueError: if ``minimum`` is supplied and ``value`` is less
than the ``minimum``
:raises MaximumValueError: if ``maximum`` is supplied and ``value`` is more
than the ``maximum``
:raises NotAnIntegerError: if ``coerce_value`` is ``False``, and ``value``
is not an integer
:raises CannotCoerceError: if ``value`` cannot be coerced to an
:class:`int <python:int>`
"""
value = numeric(value, # pylint: disable=E1123
allow_empty = allow_empty,
minimum = minimum,
maximum = maximum,
force_run = True)
if value is not None and hasattr(value, 'is_integer'):
if value.is_integer():
return int(value)
if value is not None and coerce_value:
float_value = math.ceil(value)
if is_py2:
value = int(float_value) # pylint: disable=R0204
elif is_py3:
str_value = str(float_value)
value = int(str_value, base = base)
else:
raise NotImplementedError('Python %s not supported' % os.sys.version)
elif value is not None and not isinstance(value, integer_types):
raise errors.NotAnIntegerError('value (%s) is not an integer-type, '
'is a %s'% (value, type(value))
)
return value
[docs]@disable_on_env
def float(value,
allow_empty = False,
minimum = None,
maximum = None,
**kwargs):
"""Validate that ``value`` is a :class:`float <python:float>`.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is :obj:`None <python:None>`. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is :obj:`None <python:None>`. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`float <python:float>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is :obj:`None <python:None>` and
``allow_empty`` is ``False``
:raises MinimumValueError: if ``minimum`` is supplied and ``value`` is less
than the ``minimum``
:raises MaximumValueError: if ``maximum`` is supplied and ``value`` is more
than the ``maximum``
:raises CannotCoerceError: if unable to coerce ``value`` to a
:class:`float <python:float>`
"""
try:
value = _numeric_coercion(value,
coercion_function = float_,
allow_empty = allow_empty,
minimum = minimum,
maximum = maximum)
except (errors.EmptyValueError,
errors.CannotCoerceError,
errors.MinimumValueError,
errors.MaximumValueError) as error:
raise error
except Exception as error:
raise errors.CannotCoerceError('unable to coerce value (%s) to float, '
'for an unknown reason - please see '
'stack trace' % value)
return value
[docs]@disable_on_env
def fraction(value,
allow_empty = False,
minimum = None,
maximum = None,
**kwargs):
"""Validate that ``value`` is a :class:`Fraction <python:fractions.Fraction>`.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
is :obj:`None <python:None>`. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is :obj:`None <python:None>`. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`Fraction <python:fractions.Fraction>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is :obj:`None <python:None>` and
``allow_empty`` is ``False``
:raises MinimumValueError: if ``minimum`` is supplied and ``value`` is less
than the ``minimum``
:raises MaximumValueError: if ``maximum`` is supplied and ``value`` is more
than the ``maximum``
:raises CannotCoerceError: if unable to coerce ``value`` to a
:class:`Fraction <python:fractions.Fraction>`
"""
try:
value = _numeric_coercion(value,
coercion_function = fractions.Fraction,
allow_empty = allow_empty,
minimum = minimum,
maximum = maximum)
except (errors.EmptyValueError,
errors.CannotCoerceError,
errors.MinimumValueError,
errors.MaximumValueError) as error:
raise error
except Exception as error:
raise errors.CannotCoerceError('unable to coerce value (%s) to Fraction, '
'for an unknown reason - please see '
'stack trace' % value)
return value
[docs]@disable_on_env
def decimal(value,
allow_empty = False,
minimum = None,
maximum = None,
**kwargs):
"""Validate that ``value`` is a :class:`Decimal <python:decimal.Decimal>`.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
is :obj:`None <python:None>`. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is :obj:`None <python:None>`. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param minimum: If supplied, will make sure that ``value`` is greater than or
equal to this value.
:type minimum: numeric
:param maximum: If supplied, will make sure that ``value`` is less than or
equal to this value.
:type maximum: numeric
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`Decimal <python:decimal.Decimal>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is :obj:`None <python:None>` and
``allow_empty`` is ``False``
:raises MinimumValueError: if ``minimum`` is supplied and ``value`` is less than the
``minimum``
:raises MaximumValueError: if ``maximum`` is supplied and ``value`` is more than the
``maximum``
:raises CannotCoerceError: if unable to coerce ``value`` to a
:class:`Decimal <python:decimal.Decimal>`
"""
if value is None and allow_empty:
return None
elif value is None:
raise errors.EmptyValueError('value cannot be None')
if isinstance(value, str):
try:
value = decimal_.Decimal(value.strip())
except decimal_.InvalidOperation:
raise errors.CannotCoerceError(
'value (%s) cannot be converted to a Decimal' % value
)
elif isinstance(value, fractions.Fraction):
try:
value = float(value, force_run = True) # pylint: disable=R0204, E1123
except ValueError:
raise errors.CannotCoerceError(
'value (%s) cannot be converted to a Decimal' % value
)
value = numeric(value, # pylint: disable=E1123
allow_empty = False,
maximum = maximum,
minimum = minimum,
force_run = True)
if not isinstance(value, decimal_.Decimal):
value = decimal_.Decimal(value)
return value
def _numeric_coercion(value,
coercion_function = None,
allow_empty = False,
minimum = None,
maximum = None):
"""Validate that ``value`` is numeric and coerce using ``coercion_function``.
:param value: The value to validate.
:param coercion_function: The function to use to coerce ``value`` to the desired
type.
:type coercion_function: callable
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is :obj:`None <python:None>`. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>` if
``value`` is :obj:`None <python:None>`. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: the type returned by ``coercion_function``
:raises CoercionFunctionEmptyError: if ``coercion_function`` is empty
:raises EmptyValueError: if ``value`` is :obj:`None <python:None>` and
``allow_empty`` is ``False``
:raises CannotCoerceError: if ``coercion_function`` raises an
:class:`ValueError <python:ValueError>`, :class:`TypeError <python:TypeError>`,
:class:`AttributeError <python:AttributeError>`,
:class:`IndexError <python:IndexError>, or
:class:`SyntaxError <python:SyntaxError>`
"""
if coercion_function is None:
raise errors.CoercionFunctionEmptyError('coercion_function cannot be empty')
elif not hasattr(coercion_function, '__call__'):
raise errors.NotCallableError('coercion_function must be callable')
value = numeric(value, # pylint: disable=E1123
allow_empty = allow_empty,
minimum = minimum,
maximum = maximum,
force_run = True)
if value is not None:
try:
value = coercion_function(value)
except (ValueError, TypeError, AttributeError, IndexError, SyntaxError):
raise errors.CannotCoerceError(
'cannot coerce value (%s) to desired type' % value
)
return value
## FILE-RELATED
[docs]@disable_on_env
def bytesIO(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a :class:`BytesIO <python:io.BytesIO>` object.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`BytesIO <python:io.BytesIO>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises NotBytesIOError: if ``value`` is not a :class:`BytesIO <python:io.BytesIO>`
object.
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, io.BytesIO):
raise errors.NotBytesIOError('value (%s) is not a BytesIO, '
'is a %s' % (value, type(value)))
return value
[docs]@disable_on_env
def stringIO(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a :class:`StringIO <python:io.StringIO>` object.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`StringIO <python:io.StringIO>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises NotStringIOError: if ``value`` is not a :class:`StringIO <python:io.StringIO>`
object
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, io.StringIO):
raise ValueError('value (%s) is not an io.StringIO object, '
'is a %s' % (value, type(value)))
return value
[docs]@disable_on_env
def path(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid path-like object.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: The path represented by ``value``.
:rtype: Path-like object / :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value`` is empty
:raises NotPathlikeError: if ``value`` is not a valid path-like object
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if hasattr(os, 'PathLike'):
if not isinstance(value, (str, bytes, int, os.PathLike)): # pylint: disable=E1101
raise errors.NotPathlikeError('value (%s) is path-like' % value)
else:
if not isinstance(value, int):
try:
os.path.exists(value)
except TypeError:
raise errors.NotPathlikeError('value (%s) is not path-like' % value)
return value
[docs]@disable_on_env
def path_exists(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a path-like object that exists on the local
filesystem.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: The file name represented by ``value``.
:rtype: Path-like object / :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
:raises NotPathlikeError: if ``value`` is not a path-like object
:raises PathExistsError: if ``value`` does not exist
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = path(value, force_run = True) # pylint: disable=E1123
if not os.path.exists(value):
raise errors.PathExistsError('value (%s) not found' % value)
return value
[docs]@disable_on_env
def file_exists(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid file that exists on the local filesystem.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: The file name represented by ``value``.
:rtype: Path-like object / :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
:raises NotPathlikeError: if ``value`` is not a path-like object
:raises PathExistsError: if ``value`` does not exist on the local filesystem
:raises NotAFileError: if ``value`` is not a valid file
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = path_exists(value, force_run = True) # pylint: disable=E1123
if not os.path.isfile(value):
raise errors.NotAFileError('value (%s) is not a file')
return value
[docs]@disable_on_env
def directory_exists(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid directory that exists on the local
filesystem.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: The file name represented by ``value``.
:rtype: Path-like object / :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
:raises NotPathlikeError: if ``value`` is not a path-like object
:raises PathExistsError: if ``value`` does not exist on the local filesystem
:raises NotADirectoryError: if ``value`` is not a valid directory
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = path_exists(value, force_run = True) # pylint: disable=E1123
if not os.path.isdir(value):
raise errors.NotADirectoryError('value (%s) is not a directory' % value)
return value
[docs]@disable_on_env
def readable(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a path to a readable file.
.. caution::
**Use of this validator is an anti-pattern and should be used with caution.**
Validating the readability of a file *before* attempting to read it
exposes your code to a bug called
`TOCTOU <https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use>`_.
This particular class of bug can expose your code to **security vulnerabilities**
and so this validator should only be used if you are an advanced user.
A better pattern to use when reading from a file is to apply the principle of
EAFP ("easier to ask forgiveness than permission"), and simply attempt to
write to the file using a ``try ... except`` block:
.. code-block:: python
try:
with open('path/to/filename.txt', mode = 'r') as file_object:
# read from file here
except (OSError, IOError) as error:
# Handle an error if unable to write.
:param value: The path to a file on the local filesystem whose readability
is to be validated.
:type value: Path-like object
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: Validated path-like object or :obj:`None <python:None>`
:rtype: Path-like object or :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
:raises NotPathlikeError: if ``value`` is not a path-like object
:raises PathExistsError: if ``value`` does not exist on the local filesystem
:raises NotAFileError: if ``value`` is not a valid file
:raises NotReadableError: if ``value`` cannot be opened for reading
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = file_exists(value, force_run = True) # pylint: disable=E1123
try:
with open(value, mode='r'):
pass
except (OSError, IOError):
raise errors.NotReadableError('file at %s could not be opened for '
'reading' % value)
return value
[docs]@disable_on_env
def writeable(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a path to a writeable file.
.. caution::
This validator does **NOT** work correctly on a Windows file system. This
is due to the vagaries of how Windows manages its file system and the
various ways in which it can manage file permission.
If called on a Windows file system, this validator will raise
:class:`NotImplementedError() <python:NotImplementedError>`.
.. caution::
**Use of this validator is an anti-pattern and should be used with caution.**
Validating the writability of a file *before* attempting to write to it
exposes your code to a bug called
`TOCTOU <https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use>`_.
This particular class of bug can expose your code to **security vulnerabilities**
and so this validator should only be used if you are an advanced user.
A better pattern to use when writing to file is to apply the principle of
EAFP ("easier to ask forgiveness than permission"), and simply attempt to
write to the file using a ``try ... except`` block:
.. code-block:: python
try:
with open('path/to/filename.txt', mode = 'a') as file_object:
# write to file here
except (OSError, IOError) as error:
# Handle an error if unable to write.
.. note::
This validator relies on :func:`os.access() <python:os.access>` to check
whether ``value`` is writeable. This function has certain limitations,
most especially that:
* It will **ignore** file-locking (yielding a false-positive) if the file
is locked.
* It focuses on *local operating system permissions*, which means if trying
to access a path over a network you might get a false positive or false
negative (because network paths may have more complicated authentication
methods).
:param value: The path to a file on the local filesystem whose writeability
is to be validated.
:type value: Path-like object
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: Validated absolute path or :obj:`None <python:None>`
:rtype: Path-like object or :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
:raises NotImplementedError: if used on a Windows system
:raises NotPathlikeError: if ``value`` is not a path-like object
:raises NotWriteableError: if ``value`` cannot be opened for writing
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = path(value, force_run = True)
if sys.platform in ['win32', 'cygwin']:
raise NotImplementedError('not supported on Windows')
is_valid = os.access(value, mode = os.W_OK)
if not is_valid:
raise errors.NotWriteableError('writing not allowed for file at %s' % value)
return value
[docs]@disable_on_env
def executable(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a path to an executable file.
.. caution::
This validator does **NOT** work correctly on a Windows file system. This
is due to the vagaries of how Windows manages its file system and the
various ways in which it can manage file permission.
If called on a Windows file system, this validator will raise
:class:`NotImplementedError() <python:NotImplementedError>`.
.. caution::
**Use of this validator is an anti-pattern and should be used with caution.**
Validating the executability of a file *before* attempting to execute it
exposes your code to a bug called
`TOCTOU <https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use>`_.
This particular class of bug can expose your code to **security vulnerabilities**
and so this validator should only be used if you are an advanced user.
A better pattern to use when writing to file is to apply the principle of
EAFP ("easier to ask forgiveness than permission"), and simply attempt to
execute the file using a ``try ... except`` block.
.. note::
This validator relies on :func:`os.access() <python:os.access>` to check
whether ``value`` is executable. This function has certain limitations,
most especially that:
* It will **ignore** file-locking (yielding a false-positive) if the file
is locked.
* It focuses on *local operating system permissions*, which means if trying
to access a path over a network you might get a false positive or false
negative (because network paths may have more complicated authentication
methods).
:param value: The path to a file on the local filesystem whose writeability
is to be validated.
:type value: Path-like object
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: Validated absolute path or :obj:`None <python:None>`
:rtype: Path-like object or :obj:`None <python:None>`
:raises EmptyValueError: if ``allow_empty`` is ``False`` and ``value``
is empty
:raises NotImplementedError: if used on a Windows system
:raises NotPathlikeError: if ``value`` is not a path-like object
:raises NotAFileError: if ``value`` does not exist on the local file system
:raises NotExecutableError: if ``value`` cannot be executed
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
value = file_exists(value, force_run = True)
if sys.platform in ['win32', 'cygwin']:
raise NotImplementedError('not supported on Windows')
is_valid = os.access(value, mode = os.X_OK)
if not is_valid:
raise errors.NotExecutableError('execution not allowed for file at %s' % value)
return value
## INTERNET-RELATED
[docs]@disable_on_env
def email(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid email address.
.. note::
Email address validation is...complicated. The methodology that we have
adopted here is *generally* compliant with
`RFC 5322 <https://tools.ietf.org/html/rfc5322>`_ and uses a combination of
string parsing and regular expressions.
String parsing in particular is used to validate certain *highly unusual*
but still valid email patterns, including the use of escaped text and
comments within an email address' local address (the user name part).
This approach ensures more complete coverage for unusual edge cases, while
still letting us use regular expressions that perform quickly.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` is not a :class:`str <python:str>` or
:obj:`None <python:None>`
:raises InvalidEmailError: if ``value`` is not a valid email address or
empty with ``allow_empty`` set to ``True``
"""
# pylint: disable=too-many-branches,too-many-statements,R0914
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, basestring):
raise errors.CannotCoerceError('value must be a valid string, '
'was %s' % type(value))
if '@' not in value:
raise errors.InvalidEmailError('value (%s) is not a valid email address' % value)
if '(' in value and ')' in value:
open_parentheses = value.find('(')
close_parentheses = value.find(')') + 1
if close_parentheses < open_parentheses:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
commented_value = value[open_parentheses:close_parentheses]
value = value.replace(commented_value, '')
elif '(' in value:
raise errors.InvalidEmailError('value (%s) is not a valid email address' % value)
elif ')' in value:
raise errors.InvalidEmailError('value (%s) is not a valid email address' % value)
if '<' in value or '>' in value:
lt_position = value.find('<')
gt_position = value.find('>')
first_quote_position = -1
second_quote_position = -1
if lt_position >= 0:
first_quote_position = value.find('"', 0, lt_position)
if gt_position >= 0:
second_quote_position = value.find('"', gt_position)
if first_quote_position < 0 or second_quote_position < 0:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
at_count = value.count('@')
if at_count > 1:
last_at_position = 0
last_quote_position = 0
for x in range(0, at_count): # pylint: disable=W0612
at_position = value.find('@', last_at_position + 1)
if at_position >= 0:
first_quote_position = value.find('"',
last_quote_position,
at_position)
second_quote_position = value.find('"',
first_quote_position)
if first_quote_position < 0 or second_quote_position < 0:
raise errors.InvalidEmailError(
'value (%s) is not a valid email address' % value
)
last_at_position = at_position
last_quote_position = second_quote_position
split_values = value.split('@')
if len(split_values) < 2:
raise errors.InvalidEmailError('value (%s) is not a valid email address' % value)
local_value = ''.join(split_values[:-1])
domain_value = split_values[-1]
is_domain = False
is_ip = False
try:
if domain_value.startswith('[') and domain_value.endswith(']'):
domain_value = domain_value[1:-1]
domain(domain_value)
is_domain = True
except ValueError:
is_domain = False
if not is_domain:
try:
ip_address(domain_value, force_run = True) # pylint: disable=E1123
is_ip = True
except ValueError:
is_ip = False
if not is_domain and is_ip:
try:
email(local_value + '@test.com', force_run = True) # pylint: disable=E1123
except ValueError:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
return value
if not is_domain:
raise errors.InvalidEmailError('value (%s) is not a valid email address' % value)
else:
is_valid = EMAIL_REGEX.search(value)
if not is_valid:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
matched_string = is_valid.group(0)
position = value.find(matched_string)
if position > 0:
prefix = value[:position]
if prefix[0] in string_.punctuation:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
if '..' in prefix:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
end_of_match = position + len(matched_string)
suffix = value[end_of_match:]
if suffix:
raise errors.InvalidEmailError('value (%s) is not a valid email '
'address' % value)
return value
[docs]@disable_on_env
def url(value,
allow_empty = False,
allow_special_ips = False,
**kwargs):
"""Validate that ``value`` is a valid URL.
.. note::
URL validation is...complicated. The methodology that we have
adopted here is *generally* compliant with
`RFC 1738 <https://tools.ietf.org/html/rfc1738>`_,
`RFC 6761 <https://tools.ietf.org/html/rfc6761>`_,
`RFC 2181 <https://tools.ietf.org/html/rfc2181>`_ and uses a combination of
string parsing and regular expressions,
This approach ensures more complete coverage for unusual edge cases, while
still letting us use regular expressions that perform quickly.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param allow_special_ips: If ``True``, will succeed when validating special IP
addresses, such as loopback IPs like ``127.0.0.1`` or ``0.0.0.0``. If ``False``,
will raise a :class:`InvalidURLError` if ``value`` is a special IP address. Defaults
to ``False``.
:type allow_special_ips: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` is not a :class:`str <python:str>` or
:obj:`None <python:None>`
:raises InvalidURLError: if ``value`` is not a valid URL or
empty with ``allow_empty`` set to ``True``
"""
is_recursive = kwargs.pop('is_recursive', False)
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, basestring):
raise errors.CannotCoerceError('value must be a valid string, '
'was %s' % type(value))
is_valid = False
lowercase_value = value.lower()
stripped_value = None
has_protocol = False
lowercase_stripped_value = None
for protocol in URL_PROTOCOLS:
if protocol in lowercase_value:
has_protocol = True
stripped_value = lowercase_value.replace(protocol, '')
lowercase_stripped_value = stripped_value.lower()
break
if lowercase_stripped_value:
for special_use_domain in SPECIAL_USE_DOMAIN_NAMES:
if special_use_domain in lowercase_stripped_value:
has_port = False
port_index = lowercase_stripped_value.find(':')
if port_index > -1:
has_port = True
lowercase_stripped_value = lowercase_stripped_value[:port_index]
if not has_port:
path_index = lowercase_stripped_value.find('/')
if path_index > -1:
lowercase_stripped_value = lowercase_stripped_value[:path_index]
if lowercase_stripped_value:
try:
domain(lowercase_stripped_value,
allow_empty = False,
is_recursive = is_recursive)
is_valid = True
except (ValueError, TypeError):
pass
if not is_valid and allow_special_ips:
try:
ip_address(stripped_value, allow_empty = False)
is_valid = True
except (ValueError, TypeError):
pass
if not is_valid:
is_valid = URL_REGEX.match(lowercase_value)
if is_valid:
prefix_index = value.find('@')
has_prefix = prefix_index > -1
stripped_prefix = value
if has_prefix:
stripped_prefix = stripped_prefix[prefix_index + 1:]
if has_protocol:
protocol_index = stripped_prefix.find('://')
has_protocol = protocol_index > -1
if has_protocol:
stripped_prefix = stripped_prefix[protocol_index + 3:]
port_index = stripped_prefix.find(':')
has_port = port_index > -1
if has_port:
stripped_prefix = stripped_prefix[:port_index]
else:
path_index = stripped_prefix.find('/')
if path_index > -1:
stripped_prefix = stripped_prefix[:path_index]
try:
domain(stripped_prefix,
allow_empty = False,
is_recursive = is_recursive)
except (ValueError, TypeError):
for character in URL_UNSAFE_CHARACTERS:
if character in stripped_prefix:
raise errors.InvalidURLError('value (%s) is not a valid URL' % value)
if not is_valid and allow_special_ips:
is_valid = URL_SPECIAL_IP_REGEX.match(value)
if not is_valid:
raise errors.InvalidURLError('value (%s) is not a valid URL' % value)
return value
[docs]@disable_on_env
def domain(value,
allow_empty = False,
allow_ips = False,
**kwargs):
"""Validate that ``value`` is a valid domain name.
.. caution::
This validator does not verify that ``value`` **exists** as a domain. It
merely verifies that its contents *might* exist as a domain.
.. note::
This validator checks to validate that ``value`` resembles a valid
domain name. It is - generally - compliant with
`RFC 1035 <https://tools.ietf.org/html/rfc1035>`_ and
`RFC 6761 <https://tools.ietf.org/html/rfc6761>`_, however it diverges
in a number of key ways:
* Including authentication (e.g. ``username:password@domain.dev``) will
fail validation.
* Including a path (e.g. ``domain.dev/path/to/file``) will fail validation.
* Including a port (e.g. ``domain.dev:8080``) will fail validation.
If you are hoping to validate a more complete URL, we recommend that you
see :func:`url <validator_collection.validators.url>`.
.. hint::
Leading and trailing whitespace will be automatically stripped.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:param allow_ips: If ``True``, will succeed when validating IP addresses,
If ``False``, will raise a :class:`InvalidDomainError` if ``value`` is an IP
address. Defaults to ``False``.
:type allow_ips: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` is not a :class:`str <python:str>` or
:obj:`None <python:None>`
:raises InvalidDomainError: if ``value`` is not a valid domain name or
empty with ``allow_empty`` set to ``True``
:raises SlashInDomainError: if ``value`` contains a slash or backslash
:raises AtInDomainError: if ``value`` contains an ``@`` symbol
:raises ColonInDomainError: if ``value`` contains a ``:`` symbol
:raises WhitespaceInDomainError: if ``value`` contains whitespace
"""
is_recursive = kwargs.pop('is_recursive', False)
has_unsafe_characters = False
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, basestring):
raise errors.CannotCoerceError('value must be a valid string, '
'was %s' % type(value))
if '/' in value:
raise errors.SlashInDomainError('valid domain name cannot contain "/"')
if '\\' in value:
raise errors.SlashInDomainError('valid domain name cannot contain "\\"')
if '@' in value:
raise errors.AtInDomainError('valid domain name cannot contain "@"')
if ':' in value:
raise errors.ColonInDomainError('valid domain name cannot contain ":"')
value = value.strip().lower()
for item in string_.whitespace:
if item in value:
raise errors.WhitespaceInDomainError('valid domain name cannot contain '
'whitespace')
if value in SPECIAL_USE_DOMAIN_NAMES:
return value
if allow_ips:
try:
ip_address(value, allow_empty = allow_empty)
is_valid = True
except (ValueError, TypeError, AttributeError):
is_valid = False
if is_valid:
return value
for character in URL_UNSAFE_CHARACTERS:
if character in value:
raise errors.InvalidDomainError('value (%s) is not a valid domain' % value)
is_valid = DOMAIN_REGEX.match(value)
if not is_valid and not is_recursive:
with_prefix = 'http://' + value
try:
url(with_prefix, force_run = True, is_recursive = True) # pylint: disable=E1123
except ValueError:
raise errors.InvalidDomainError('value (%s) is not a valid domain' % value)
elif not is_valid:
raise errors.InvalidDomainError('value (%s) is not a valid domain' % value)
return value
[docs]@disable_on_env
def ip_address(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid IP address.
.. note::
First, the validator will check if the address is a valid IPv6 address.
If that doesn't work, the validator will check if the address is a valid
IPv4 address.
If neither works, the validator will raise an error (as always).
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises InvalidIPAddressError: if ``value`` is not a valid IP address or empty with
``allow_empty`` set to ``True``
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if is_py2 and value and isinstance(value, unicode):
value = value.encode('utf-8')
try:
value = ipv6(value, force_run = True) # pylint: disable=E1123
ipv6_failed = False
except ValueError:
ipv6_failed = True
if ipv6_failed:
try:
value = ipv4(value, force_run = True) # pylint: disable=E1123
except ValueError:
raise errors.InvalidIPAddressError('value (%s) is not a valid IPv6 or '
'IPv4 address' % value)
return value
[docs]@disable_on_env
def ipv4(value, allow_empty = False):
"""Validate that ``value`` is a valid IP version 4 address.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises InvalidIPAddressError: if ``value`` is not a valid IP version 4 address or
empty with ``allow_empty`` set to ``True``
"""
if not value and allow_empty is False:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
try:
components = value.split('.')
except AttributeError:
raise errors.InvalidIPAddressError('value (%s) is not a valid ipv4' % value)
if len(components) != 4 or not all(x.isdigit() for x in components):
raise errors.InvalidIPAddressError('value (%s) is not a valid ipv4' % value)
for x in components:
try:
x = integer(x,
minimum = 0,
maximum = 255)
except ValueError:
raise errors.InvalidIPAddressError('value (%s) is not a valid ipv4' % value)
return value
[docs]@disable_on_env
def ipv6(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid IP address version 6.
:param value: The value to validate.
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises InvalidIPAddressError: if ``value`` is not a valid IP version 6 address or
empty with ``allow_empty`` is not set to ``True``
"""
if not value and allow_empty is False:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, str):
raise errors.InvalidIPAddressError('value (%s) is not a valid ipv6' % value)
value = value.lower().strip()
is_valid = IPV6_REGEX.match(value)
if not is_valid:
raise errors.InvalidIPAddressError('value (%s) is not a valid ipv6' % value)
return value
[docs]@disable_on_env
def mac_address(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid MAC address.
:param value: The value to validate.
:type value: :class:`str <python:str>` / :obj:`None <python:None>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` is not a valid :class:`str <python:str>`
or string-like object
:raises InvalidMACAddressError: if ``value`` is not a valid MAC address or empty with
``allow_empty`` set to ``True``
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, basestring):
raise errors.CannotCoerceError('value must be a valid string, '
'was %s' % type(value))
if '-' in value:
value = value.replace('-', ':')
value = value.lower().strip()
is_valid = MAC_ADDRESS_REGEX.match(value)
if not is_valid:
raise errors.InvalidMACAddressError('value (%s) is not a valid MAC '
'address' % value)
return value
[docs]@disable_on_env
def mimetype(value,
allow_empty = False,
**kwargs):
"""Validate that ``value`` is a valid MIME-type.
:param value: The value to validate.
:type value: :class:`str <python:str>`
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
``value`` is empty. If ``False``, raises a
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
if ``value`` is empty. Defaults to ``False``.
:type allow_empty: :class:`bool <python:bool>`
:returns: ``value`` / :obj:`None <python:None>`
:rtype: :class:`str <python:str>` / :obj:`None <python:None>`
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
:raises CannotCoerceError: if ``value`` is not a valid string
:raises InvalidMimeTypeError: if ``value`` is neither a valid MIME type nor empty
with ``allow_empty`` set to ``True``
"""
if not value and not allow_empty:
raise errors.EmptyValueError('value (%s) was empty' % value)
elif not value:
return None
if not isinstance(value, basestring):
raise errors.CannotCoerceError('value must be a valid string, '
'was %s' % type(value))
value = value.lower().strip()
is_valid = MIME_TYPE_REGEX.fullmatch(value)
if not is_valid:
raise errors.InvalidMimeTypeError(
'value (%s) is not a valid MIME Type' % value
)
return value