|
|
|
from calendar import timegm
|
|
|
|
from datetime import datetime, time, timedelta
|
|
|
|
from email.utils import parsedate_tz, mktime_tz
|
|
|
|
import re
|
|
|
|
|
|
|
|
import aniso8601
|
|
|
|
import pytz
|
|
|
|
|
|
|
|
# Constants for upgrading date-based intervals to full datetimes.
|
|
|
|
START_OF_DAY = time(0, 0, 0, tzinfo=pytz.UTC)
|
|
|
|
END_OF_DAY = time(23, 59, 59, 999999, tzinfo=pytz.UTC)
|
|
|
|
|
|
|
|
# https://code.djangoproject.com/browser/django/trunk/django/core/validators.py
|
|
|
|
# basic auth added by frank
|
|
|
|
|
|
|
|
url_regex = re.compile(
|
|
|
|
r'^(?:http|ftp)s?://' # http:// or https://
|
|
|
|
r'(?:[^:@]+?:[^:@]*?@|)' # basic auth
|
|
|
|
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
|
|
|
|
r'(?:[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)
|
|
|
|
|
|
|
|
|
|
|
|
def url(value):
|
|
|
|
"""Validate a URL.
|
|
|
|
|
|
|
|
:param string value: The URL to validate
|
|
|
|
:returns: The URL if valid.
|
|
|
|
:raises: ValueError
|
|
|
|
"""
|
|
|
|
if not url_regex.search(value):
|
|
|
|
message = u"{0} is not a valid URL".format(value)
|
|
|
|
if url_regex.search('http://' + value):
|
|
|
|
message += u". Did you mean: http://{0}".format(value)
|
|
|
|
raise ValueError(message)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
class regex(object):
|
|
|
|
"""Validate a string based on a regular expression.
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
parser = reqparse.RequestParser()
|
|
|
|
parser.add_argument('example', type=inputs.regex('^[0-9]+$'))
|
|
|
|
|
|
|
|
Input to the ``example`` argument will be rejected if it contains anything
|
|
|
|
but numbers.
|
|
|
|
|
|
|
|
:param pattern: The regular expression the input must match
|
|
|
|
:type pattern: str
|
|
|
|
:param flags: Flags to change expression behavior
|
|
|
|
:type flags: int
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, pattern, flags=0):
|
|
|
|
self.pattern = pattern
|
|
|
|
self.re = re.compile(pattern, flags)
|
|
|
|
|
|
|
|
def __call__(self, value):
|
|
|
|
if not self.re.search(value):
|
|
|
|
message = 'Value does not match pattern: "{0}"'.format(self.pattern)
|
|
|
|
raise ValueError(message)
|
|
|
|
return value
|
|
|
|
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
return regex(self.pattern)
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_interval(start, end, value):
|
|
|
|
"""Normalize datetime intervals.
|
|
|
|
|
|
|
|
Given a pair of datetime.date or datetime.datetime objects,
|
|
|
|
returns a 2-tuple of tz-aware UTC datetimes spanning the same interval.
|
|
|
|
|
|
|
|
For datetime.date objects, the returned interval starts at 00:00:00.0
|
|
|
|
on the first date and ends at 00:00:00.0 on the second.
|
|
|
|
|
|
|
|
Naive datetimes are upgraded to UTC.
|
|
|
|
|
|
|
|
Timezone-aware datetimes are normalized to the UTC tzdata.
|
|
|
|
|
|
|
|
Params:
|
|
|
|
- start: A date or datetime
|
|
|
|
- end: A date or datetime
|
|
|
|
"""
|
|
|
|
if not isinstance(start, datetime):
|
|
|
|
start = datetime.combine(start, START_OF_DAY)
|
|
|
|
end = datetime.combine(end, START_OF_DAY)
|
|
|
|
|
|
|
|
if start.tzinfo is None:
|
|
|
|
start = pytz.UTC.localize(start)
|
|
|
|
end = pytz.UTC.localize(end)
|
|
|
|
else:
|
|
|
|
start = start.astimezone(pytz.UTC)
|
|
|
|
end = end.astimezone(pytz.UTC)
|
|
|
|
|
|
|
|
return start, end
|
|
|
|
|
|
|
|
|
|
|
|
def _expand_datetime(start, value):
|
|
|
|
if not isinstance(start, datetime):
|
|
|
|
# Expand a single date object to be the interval spanning
|
|
|
|
# that entire day.
|
|
|
|
end = start + timedelta(days=1)
|
|
|
|
else:
|
|
|
|
# Expand a datetime based on the finest resolution provided
|
|
|
|
# in the original input string.
|
|
|
|
time = value.split('T')[1]
|
|
|
|
time_without_offset = re.sub('[+-].+', '', time)
|
|
|
|
num_separators = time_without_offset.count(':')
|
|
|
|
if num_separators == 0:
|
|
|
|
# Hour resolution
|
|
|
|
end = start + timedelta(hours=1)
|
|
|
|
elif num_separators == 1:
|
|
|
|
# Minute resolution:
|
|
|
|
end = start + timedelta(minutes=1)
|
|
|
|
else:
|
|
|
|
# Second resolution
|
|
|
|
end = start + timedelta(seconds=1)
|
|
|
|
|
|
|
|
return end
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_interval(value):
|
|
|
|
"""Do some nasty try/except voodoo to get some sort of datetime
|
|
|
|
object(s) out of the string.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return sorted(aniso8601.parse_interval(value))
|
|
|
|
except ValueError:
|
|
|
|
try:
|
|
|
|
return aniso8601.parse_datetime(value), None
|
|
|
|
except ValueError:
|
|
|
|
return aniso8601.parse_date(value), None
|
|
|
|
|
|
|
|
|
|
|
|
def iso8601interval(value, argument='argument'):
|
|
|
|
"""Parses ISO 8601-formatted datetime intervals into tuples of datetimes.
|
|
|
|
|
|
|
|
Accepts both a single date(time) or a full interval using either start/end
|
|
|
|
or start/duration notation, with the following behavior:
|
|
|
|
|
|
|
|
- Intervals are defined as inclusive start, exclusive end
|
|
|
|
- Single datetimes are translated into the interval spanning the
|
|
|
|
largest resolution not specified in the input value, up to the day.
|
|
|
|
- The smallest accepted resolution is 1 second.
|
|
|
|
- All timezones are accepted as values; returned datetimes are
|
|
|
|
localized to UTC. Naive inputs and date inputs will are assumed UTC.
|
|
|
|
|
|
|
|
Examples::
|
|
|
|
|
|
|
|
"2013-01-01" -> datetime(2013, 1, 1), datetime(2013, 1, 2)
|
|
|
|
"2013-01-01T12" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 13)
|
|
|
|
"2013-01-01/2013-02-28" -> datetime(2013, 1, 1), datetime(2013, 2, 28)
|
|
|
|
"2013-01-01/P3D" -> datetime(2013, 1, 1), datetime(2013, 1, 4)
|
|
|
|
"2013-01-01T12:00/PT30M" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 12, 30)
|
|
|
|
"2013-01-01T06:00/2013-01-01T12:00" -> datetime(2013, 1, 1, 6), datetime(2013, 1, 1, 12)
|
|
|
|
|
|
|
|
:param str value: The ISO8601 date time as a string
|
|
|
|
:return: Two UTC datetimes, the start and the end of the specified interval
|
|
|
|
:rtype: A tuple (datetime, datetime)
|
|
|
|
:raises: ValueError, if the interval is invalid.
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
start, end = _parse_interval(value)
|
|
|
|
|
|
|
|
if end is None:
|
|
|
|
end = _expand_datetime(start, value)
|
|
|
|
|
|
|
|
start, end = _normalize_interval(start, end, value)
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError(
|
|
|
|
"Invalid {arg}: {value}. {arg} must be a valid ISO8601 "
|
|
|
|
"date/time interval.".format(arg=argument, value=value),
|
|
|
|
)
|
|
|
|
|
|
|
|
return start, end
|
|
|
|
|
|
|
|
|
|
|
|
def date(value):
|
|
|
|
"""Parse a valid looking date in the format YYYY-mm-dd"""
|
|
|
|
date = datetime.strptime(value, "%Y-%m-%d")
|
|
|
|
return date
|
|
|
|
|
|
|
|
|
|
|
|
def _get_integer(value):
|
|
|
|
try:
|
|
|
|
return int(value)
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
raise ValueError('{0} is not a valid integer'.format(value))
|
|
|
|
|
|
|
|
|
|
|
|
def natural(value, argument='argument'):
|
|
|
|
""" Restrict input type to the natural numbers (0, 1, 2, 3...) """
|
|
|
|
value = _get_integer(value)
|
|
|
|
if value < 0:
|
|
|
|
error = ('Invalid {arg}: {value}. {arg} must be a non-negative '
|
|
|
|
'integer'.format(arg=argument, value=value))
|
|
|
|
raise ValueError(error)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def positive(value, argument='argument'):
|
|
|
|
""" Restrict input type to the positive integers (1, 2, 3...) """
|
|
|
|
value = _get_integer(value)
|
|
|
|
if value < 1:
|
|
|
|
error = ('Invalid {arg}: {value}. {arg} must be a positive '
|
|
|
|
'integer'.format(arg=argument, value=value))
|
|
|
|
raise ValueError(error)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
class int_range(object):
|
|
|
|
""" Restrict input to an integer in a range (inclusive) """
|
|
|
|
def __init__(self, low, high, argument='argument'):
|
|
|
|
self.low = low
|
|
|
|
self.high = high
|
|
|
|
self.argument = argument
|
|
|
|
|
|
|
|
def __call__(self, value):
|
|
|
|
value = _get_integer(value)
|
|
|
|
if value < self.low or value > self.high:
|
|
|
|
error = ('Invalid {arg}: {val}. {arg} must be within the range {lo} - {hi}'
|
|
|
|
.format(arg=self.argument, val=value, lo=self.low, hi=self.high))
|
|
|
|
raise ValueError(error)
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def boolean(value):
|
|
|
|
"""Parse the string ``"true"`` or ``"false"`` as a boolean (case
|
|
|
|
insensitive). Also accepts ``"1"`` and ``"0"`` as ``True``/``False``
|
|
|
|
(respectively). If the input is from the request JSON body, the type is
|
|
|
|
already a native python boolean, and will be passed through without
|
|
|
|
further parsing.
|
|
|
|
"""
|
|
|
|
if isinstance(value, bool):
|
|
|
|
return value
|
|
|
|
|
|
|
|
if not value:
|
|
|
|
raise ValueError("boolean type must be non-null")
|
|
|
|
value = value.lower()
|
|
|
|
if value in ('true', '1',):
|
|
|
|
return True
|
|
|
|
if value in ('false', '0',):
|
|
|
|
return False
|
|
|
|
raise ValueError("Invalid literal for boolean(): {0}".format(value))
|
|
|
|
|
|
|
|
|
|
|
|
def datetime_from_rfc822(datetime_str):
|
|
|
|
"""Turns an RFC822 formatted date into a datetime object.
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
inputs.datetime_from_rfc822("Wed, 02 Oct 2002 08:00:00 EST")
|
|
|
|
|
|
|
|
:param datetime_str: The RFC822-complying string to transform
|
|
|
|
:type datetime_str: str
|
|
|
|
:return: A datetime
|
|
|
|
"""
|
|
|
|
return datetime.fromtimestamp(mktime_tz(parsedate_tz(datetime_str)), pytz.utc)
|
|
|
|
|
|
|
|
|
|
|
|
def datetime_from_iso8601(datetime_str):
|
|
|
|
"""Turns an ISO8601 formatted datetime into a datetime object.
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
inputs.datetime_from_iso8601("2012-01-01T23:30:00+02:00")
|
|
|
|
|
|
|
|
:param datetime_str: The ISO8601-complying string to transform
|
|
|
|
:type datetime_str: str
|
|
|
|
:return: A datetime
|
|
|
|
"""
|
|
|
|
return aniso8601.parse_datetime(datetime_str)
|