inputs.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. from calendar import timegm
  2. from datetime import datetime, time, timedelta
  3. from email.utils import parsedate_tz, mktime_tz
  4. import re
  5. import aniso8601
  6. import pytz
  7. # Constants for upgrading date-based intervals to full datetimes.
  8. START_OF_DAY = time(0, 0, 0, tzinfo=pytz.UTC)
  9. END_OF_DAY = time(23, 59, 59, 999999, tzinfo=pytz.UTC)
  10. # https://code.djangoproject.com/browser/django/trunk/django/core/validators.py
  11. # basic auth added by frank
  12. url_regex = re.compile(
  13. r'^(?:http|ftp)s?://' # http:// or https://
  14. r'(?:[^:@]+?:[^:@]*?@|)' # basic auth
  15. r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
  16. r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
  17. r'localhost|' # localhost...
  18. r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
  19. r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
  20. r'(?::\d+)?' # optional port
  21. r'(?:/?|[/?]\S+)$', re.IGNORECASE)
  22. def url(value):
  23. """Validate a URL.
  24. :param string value: The URL to validate
  25. :returns: The URL if valid.
  26. :raises: ValueError
  27. """
  28. if not url_regex.search(value):
  29. message = u"{0} is not a valid URL".format(value)
  30. if url_regex.search('http://' + value):
  31. message += u". Did you mean: http://{0}".format(value)
  32. raise ValueError(message)
  33. return value
  34. class regex(object):
  35. """Validate a string based on a regular expression.
  36. Example::
  37. parser = reqparse.RequestParser()
  38. parser.add_argument('example', type=inputs.regex('^[0-9]+$'))
  39. Input to the ``example`` argument will be rejected if it contains anything
  40. but numbers.
  41. :param pattern: The regular expression the input must match
  42. :type pattern: str
  43. :param flags: Flags to change expression behavior
  44. :type flags: int
  45. """
  46. def __init__(self, pattern, flags=0):
  47. self.pattern = pattern
  48. self.re = re.compile(pattern, flags)
  49. def __call__(self, value):
  50. if not self.re.search(value):
  51. message = 'Value does not match pattern: "{0}"'.format(self.pattern)
  52. raise ValueError(message)
  53. return value
  54. def __deepcopy__(self, memo):
  55. return regex(self.pattern)
  56. def _normalize_interval(start, end, value):
  57. """Normalize datetime intervals.
  58. Given a pair of datetime.date or datetime.datetime objects,
  59. returns a 2-tuple of tz-aware UTC datetimes spanning the same interval.
  60. For datetime.date objects, the returned interval starts at 00:00:00.0
  61. on the first date and ends at 00:00:00.0 on the second.
  62. Naive datetimes are upgraded to UTC.
  63. Timezone-aware datetimes are normalized to the UTC tzdata.
  64. Params:
  65. - start: A date or datetime
  66. - end: A date or datetime
  67. """
  68. if not isinstance(start, datetime):
  69. start = datetime.combine(start, START_OF_DAY)
  70. end = datetime.combine(end, START_OF_DAY)
  71. if start.tzinfo is None:
  72. start = pytz.UTC.localize(start)
  73. end = pytz.UTC.localize(end)
  74. else:
  75. start = start.astimezone(pytz.UTC)
  76. end = end.astimezone(pytz.UTC)
  77. return start, end
  78. def _expand_datetime(start, value):
  79. if not isinstance(start, datetime):
  80. # Expand a single date object to be the interval spanning
  81. # that entire day.
  82. end = start + timedelta(days=1)
  83. else:
  84. # Expand a datetime based on the finest resolution provided
  85. # in the original input string.
  86. time = value.split('T')[1]
  87. time_without_offset = re.sub('[+-].+', '', time)
  88. num_separators = time_without_offset.count(':')
  89. if num_separators == 0:
  90. # Hour resolution
  91. end = start + timedelta(hours=1)
  92. elif num_separators == 1:
  93. # Minute resolution:
  94. end = start + timedelta(minutes=1)
  95. else:
  96. # Second resolution
  97. end = start + timedelta(seconds=1)
  98. return end
  99. def _parse_interval(value):
  100. """Do some nasty try/except voodoo to get some sort of datetime
  101. object(s) out of the string.
  102. """
  103. try:
  104. return sorted(aniso8601.parse_interval(value))
  105. except ValueError:
  106. try:
  107. return aniso8601.parse_datetime(value), None
  108. except ValueError:
  109. return aniso8601.parse_date(value), None
  110. def iso8601interval(value, argument='argument'):
  111. """Parses ISO 8601-formatted datetime intervals into tuples of datetimes.
  112. Accepts both a single date(time) or a full interval using either start/end
  113. or start/duration notation, with the following behavior:
  114. - Intervals are defined as inclusive start, exclusive end
  115. - Single datetimes are translated into the interval spanning the
  116. largest resolution not specified in the input value, up to the day.
  117. - The smallest accepted resolution is 1 second.
  118. - All timezones are accepted as values; returned datetimes are
  119. localized to UTC. Naive inputs and date inputs will are assumed UTC.
  120. Examples::
  121. "2013-01-01" -> datetime(2013, 1, 1), datetime(2013, 1, 2)
  122. "2013-01-01T12" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 13)
  123. "2013-01-01/2013-02-28" -> datetime(2013, 1, 1), datetime(2013, 2, 28)
  124. "2013-01-01/P3D" -> datetime(2013, 1, 1), datetime(2013, 1, 4)
  125. "2013-01-01T12:00/PT30M" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 12, 30)
  126. "2013-01-01T06:00/2013-01-01T12:00" -> datetime(2013, 1, 1, 6), datetime(2013, 1, 1, 12)
  127. :param str value: The ISO8601 date time as a string
  128. :return: Two UTC datetimes, the start and the end of the specified interval
  129. :rtype: A tuple (datetime, datetime)
  130. :raises: ValueError, if the interval is invalid.
  131. """
  132. try:
  133. start, end = _parse_interval(value)
  134. if end is None:
  135. end = _expand_datetime(start, value)
  136. start, end = _normalize_interval(start, end, value)
  137. except ValueError:
  138. raise ValueError(
  139. "Invalid {arg}: {value}. {arg} must be a valid ISO8601 "
  140. "date/time interval.".format(arg=argument, value=value),
  141. )
  142. return start, end
  143. def date(value):
  144. """Parse a valid looking date in the format YYYY-mm-dd"""
  145. date = datetime.strptime(value, "%Y-%m-%d")
  146. return date
  147. def _get_integer(value):
  148. try:
  149. return int(value)
  150. except (TypeError, ValueError):
  151. raise ValueError('{0} is not a valid integer'.format(value))
  152. def natural(value, argument='argument'):
  153. """ Restrict input type to the natural numbers (0, 1, 2, 3...) """
  154. value = _get_integer(value)
  155. if value < 0:
  156. error = ('Invalid {arg}: {value}. {arg} must be a non-negative '
  157. 'integer'.format(arg=argument, value=value))
  158. raise ValueError(error)
  159. return value
  160. def positive(value, argument='argument'):
  161. """ Restrict input type to the positive integers (1, 2, 3...) """
  162. value = _get_integer(value)
  163. if value < 1:
  164. error = ('Invalid {arg}: {value}. {arg} must be a positive '
  165. 'integer'.format(arg=argument, value=value))
  166. raise ValueError(error)
  167. return value
  168. class int_range(object):
  169. """ Restrict input to an integer in a range (inclusive) """
  170. def __init__(self, low, high, argument='argument'):
  171. self.low = low
  172. self.high = high
  173. self.argument = argument
  174. def __call__(self, value):
  175. value = _get_integer(value)
  176. if value < self.low or value > self.high:
  177. error = ('Invalid {arg}: {val}. {arg} must be within the range {lo} - {hi}'
  178. .format(arg=self.argument, val=value, lo=self.low, hi=self.high))
  179. raise ValueError(error)
  180. return value
  181. def boolean(value):
  182. """Parse the string ``"true"`` or ``"false"`` as a boolean (case
  183. insensitive). Also accepts ``"1"`` and ``"0"`` as ``True``/``False``
  184. (respectively). If the input is from the request JSON body, the type is
  185. already a native python boolean, and will be passed through without
  186. further parsing.
  187. """
  188. if isinstance(value, bool):
  189. return value
  190. if not value:
  191. raise ValueError("boolean type must be non-null")
  192. value = value.lower()
  193. if value in ('true', '1',):
  194. return True
  195. if value in ('false', '0',):
  196. return False
  197. raise ValueError("Invalid literal for boolean(): {0}".format(value))
  198. def datetime_from_rfc822(datetime_str):
  199. """Turns an RFC822 formatted date into a datetime object.
  200. Example::
  201. inputs.datetime_from_rfc822("Wed, 02 Oct 2002 08:00:00 EST")
  202. :param datetime_str: The RFC822-complying string to transform
  203. :type datetime_str: str
  204. :return: A datetime
  205. """
  206. return datetime.fromtimestamp(mktime_tz(parsedate_tz(datetime_str)), pytz.utc)
  207. def datetime_from_iso8601(datetime_str):
  208. """Turns an ISO8601 formatted datetime into a datetime object.
  209. Example::
  210. inputs.datetime_from_iso8601("2012-01-01T23:30:00+02:00")
  211. :param datetime_str: The ISO8601-complying string to transform
  212. :type datetime_str: str
  213. :return: A datetime
  214. """
  215. return aniso8601.parse_datetime(datetime_str)