123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- # -*- coding: utf-8 -*-
- """
- This module provide some helpers for advanced types parsing.
- You can define you own parser using the same pattern:
- .. code-block:: python
- def my_type(value):
- if not condition:
- raise ValueError('This is not my type')
- return parse(value)
- # Swagger documentation
- my_type.__schema__ = {'type': 'string', 'format': 'my-custom-format'}
- The last line allows you to document properly the type in the Swagger documentation.
- """
- from __future__ import unicode_literals
- import re
- import socket
- from datetime import datetime, time, timedelta
- from email.utils import parsedate_tz, mktime_tz
- from six.moves.urllib.parse import urlparse
- 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)
- netloc_regex = re.compile(
- r"(?:(?P<auth>[^:@]+?(?::[^:@]*?)?)@)?" # basic auth
- r"(?:"
- r"(?P<localhost>localhost)|" # localhost...
- r"(?P<ipv4>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|" # ...or ipv4
- r"(?:\[?(?P<ipv6>[A-F0-9]*:[A-F0-9:]+)\]?)|" # ...or ipv6
- r"(?P<domain>(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))" # domain...
- r")"
- r"(?::(?P<port>\d+))?" # optional port
- r"$",
- re.IGNORECASE,
- )
- email_regex = re.compile(
- r"^" "(?P<local>[^@]*[^@.])" r"@" r"(?P<server>[^@\.]+(?:\.[^@\.]+)*)" r"$",
- re.IGNORECASE,
- )
- time_regex = re.compile(r"\d{2}:\d{2}")
- def ipv4(value):
- """Validate an IPv4 address"""
- try:
- socket.inet_aton(value)
- if value.count(".") == 3:
- return value
- except socket.error:
- pass
- raise ValueError("{0} is not a valid ipv4 address".format(value))
- ipv4.__schema__ = {"type": "string", "format": "ipv4"}
- def ipv6(value):
- """Validate an IPv6 address"""
- try:
- socket.inet_pton(socket.AF_INET6, value)
- return value
- except socket.error:
- raise ValueError("{0} is not a valid ipv4 address".format(value))
- ipv6.__schema__ = {"type": "string", "format": "ipv6"}
- def ip(value):
- """Validate an IP address (both IPv4 and IPv6)"""
- try:
- return ipv4(value)
- except ValueError:
- pass
- try:
- return ipv6(value)
- except ValueError:
- raise ValueError("{0} is not a valid ip".format(value))
- ip.__schema__ = {"type": "string", "format": "ip"}
- class URL(object):
- """
- Validate an URL.
- Example::
- parser = reqparse.RequestParser()
- parser.add_argument('url', type=inputs.URL(schemes=['http', 'https']))
- Input to the ``URL`` argument will be rejected
- if it does not match an URL with specified constraints.
- If ``check`` is True it will also be rejected if the domain does not exists.
- :param bool check: Check the domain exists (perform a DNS resolution)
- :param bool ip: Allow IP (both ipv4/ipv6) as domain
- :param bool local: Allow localhost (both string or ip) as domain
- :param bool port: Allow a port to be present
- :param bool auth: Allow authentication to be present
- :param list|tuple schemes: Restrict valid schemes to this list
- :param list|tuple domains: Restrict valid domains to this list
- :param list|tuple exclude: Exclude some domains
- """
- def __init__(
- self,
- check=False,
- ip=False,
- local=False,
- port=False,
- auth=False,
- schemes=None,
- domains=None,
- exclude=None,
- ):
- self.check = check
- self.ip = ip
- self.local = local
- self.port = port
- self.auth = auth
- self.schemes = schemes
- self.domains = domains
- self.exclude = exclude
- def error(self, value, details=None):
- msg = "{0} is not a valid URL"
- if details:
- msg = ". ".join((msg, details))
- raise ValueError(msg.format(value))
- def __call__(self, value):
- parsed = urlparse(value)
- netloc_match = netloc_regex.match(parsed.netloc)
- if not all((parsed.scheme, parsed.netloc)):
- if netloc_regex.match(
- parsed.netloc or parsed.path.split("/", 1)[0].split("?", 1)[0]
- ):
- self.error(value, "Did you mean: http://{0}")
- self.error(value)
- if parsed.scheme and self.schemes and parsed.scheme not in self.schemes:
- self.error(value, "Protocol is not allowed")
- if not netloc_match:
- self.error(value)
- data = netloc_match.groupdict()
- if data["ipv4"] or data["ipv6"]:
- if not self.ip:
- self.error(value, "IP is not allowed")
- else:
- try:
- ip(data["ipv4"] or data["ipv6"])
- except ValueError as e:
- self.error(value, str(e))
- if not self.local:
- if data["ipv4"] and data["ipv4"].startswith("127."):
- self.error(value, "Localhost is not allowed")
- elif data["ipv6"] == "::1":
- self.error(value, "Localhost is not allowed")
- if self.check:
- pass
- if data["auth"] and not self.auth:
- self.error(value, "Authentication is not allowed")
- if data["localhost"] and not self.local:
- self.error(value, "Localhost is not allowed")
- if data["port"]:
- if not self.port:
- self.error(value, "Custom port is not allowed")
- else:
- port = int(data["port"])
- if not 0 < port < 65535:
- self.error(value, "Port is out of range")
- if data["domain"]:
- if self.domains and data["domain"] not in self.domains:
- self.error(value, "Domain is not allowed")
- elif self.exclude and data["domain"] in self.exclude:
- self.error(value, "Domain is not allowed")
- if self.check:
- try:
- socket.getaddrinfo(data["domain"], None)
- except socket.error:
- self.error(value, "Domain does not exists")
- return value
- @property
- def __schema__(self):
- return {
- "type": "string",
- "format": "url",
- }
- #: Validate an URL
- #:
- #: Legacy validator, allows, auth, port, ip and local
- #: Only allows schemes 'http', 'https', 'ftp' and 'ftps'
- url = URL(
- ip=True, auth=True, port=True, local=True, schemes=("http", "https", "ftp", "ftps")
- )
- class email(object):
- """
- Validate an email.
- Example::
- parser = reqparse.RequestParser()
- parser.add_argument('email', type=inputs.email(dns=True))
- Input to the ``email`` argument will be rejected if it does not match an email
- and if domain does not exists.
- :param bool check: Check the domain exists (perform a DNS resolution)
- :param bool ip: Allow IP (both ipv4/ipv6) as domain
- :param bool local: Allow localhost (both string or ip) as domain
- :param list|tuple domains: Restrict valid domains to this list
- :param list|tuple exclude: Exclude some domains
- """
- def __init__(self, check=False, ip=False, local=False, domains=None, exclude=None):
- self.check = check
- self.ip = ip
- self.local = local
- self.domains = domains
- self.exclude = exclude
- def error(self, value, msg=None):
- msg = msg or "{0} is not a valid email"
- raise ValueError(msg.format(value))
- def is_ip(self, value):
- try:
- ip(value)
- return True
- except ValueError:
- return False
- def __call__(self, value):
- match = email_regex.match(value)
- if not match or ".." in value:
- self.error(value)
- server = match.group("server")
- if self.check:
- try:
- socket.getaddrinfo(server, None)
- except socket.error:
- self.error(value)
- if self.domains and server not in self.domains:
- self.error(value, "{0} does not belong to the authorized domains")
- if self.exclude and server in self.exclude:
- self.error(value, "{0} belongs to a forbidden domain")
- if not self.local and (
- server in ("localhost", "::1") or server.startswith("127.")
- ):
- self.error(value)
- if self.is_ip(server) and not self.ip:
- self.error(value)
- return value
- @property
- def __schema__(self):
- return {
- "type": "string",
- "format": "email",
- }
- 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 str pattern: The regular expression the input must match
- """
- def __init__(self, pattern):
- self.pattern = pattern
- self.re = re.compile(pattern)
- 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)
- @property
- def __schema__(self):
- return {
- "type": "string",
- "pattern": 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.
- """
- if not value:
- raise ValueError("Expected a valid ISO8601 date/time interval.")
- try:
- start, end = _parse_interval(value)
- if end is None:
- end = _expand_datetime(start, value)
- start, end = _normalize_interval(start, end, value)
- except ValueError:
- msg = (
- "Invalid {arg}: {value}. {arg} must be a valid ISO8601 date/time interval."
- )
- raise ValueError(msg.format(arg=argument, value=value))
- return start, end
- iso8601interval.__schema__ = {"type": "string", "format": "iso8601-interval"}
- def date(value):
- """Parse a valid looking date in the format YYYY-mm-dd"""
- date = datetime.strptime(value, "%Y-%m-%d")
- return date
- date.__schema__ = {"type": "string", "format": "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:
- msg = "Invalid {arg}: {value}. {arg} must be a non-negative integer"
- raise ValueError(msg.format(arg=argument, value=value))
- return value
- natural.__schema__ = {"type": "integer", "minimum": 0}
- def positive(value, argument="argument"):
- """Restrict input type to the positive integers (1, 2, 3...)"""
- value = _get_integer(value)
- if value < 1:
- msg = "Invalid {arg}: {value}. {arg} must be a positive integer"
- raise ValueError(msg.format(arg=argument, value=value))
- return value
- positive.__schema__ = {"type": "integer", "minimum": 0, "exclusiveMinimum": True}
- 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:
- msg = "Invalid {arg}: {val}. {arg} must be within the range {lo} - {hi}"
- raise ValueError(
- msg.format(arg=self.argument, val=value, lo=self.low, hi=self.high)
- )
- return value
- @property
- def __schema__(self):
- return {
- "type": "integer",
- "minimum": self.low,
- "maximum": self.high,
- }
- 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.
- :raises ValueError: if the boolean value is invalid
- """
- if isinstance(value, bool):
- return value
- if value is None:
- raise ValueError("boolean type must be non-null")
- elif not value:
- return False
- value = str(value).lower()
- if value in ("true", "1", "on",):
- return True
- if value in ("false", "0",):
- return False
- raise ValueError("Invalid literal for boolean(): {0}".format(value))
- boolean.__schema__ = {"type": "boolean"}
- def datetime_from_rfc822(value):
- """
- Turns an RFC822 formatted date into a datetime object.
- Example::
- inputs.datetime_from_rfc822('Wed, 02 Oct 2002 08:00:00 EST')
- :param str value: The RFC822-complying string to transform
- :return: The parsed datetime
- :rtype: datetime
- :raises ValueError: if value is an invalid date literal
- """
- raw = value
- if not time_regex.search(value):
- value = " ".join((value, "00:00:00"))
- try:
- timetuple = parsedate_tz(value)
- timestamp = mktime_tz(timetuple)
- if timetuple[-1] is None:
- return datetime.fromtimestamp(timestamp).replace(tzinfo=pytz.utc)
- else:
- return datetime.fromtimestamp(timestamp, pytz.utc)
- except Exception:
- raise ValueError('Invalid date literal "{0}"'.format(raw))
- def datetime_from_iso8601(value):
- """
- Turns an ISO8601 formatted date into a datetime object.
- Example::
- inputs.datetime_from_iso8601("2012-01-01T23:30:00+02:00")
- :param str value: The ISO8601-complying string to transform
- :return: A datetime
- :rtype: datetime
- :raises ValueError: if value is an invalid date literal
- """
- try:
- try:
- return aniso8601.parse_datetime(value)
- except ValueError:
- date = aniso8601.parse_date(value)
- return datetime(date.year, date.month, date.day)
- except Exception:
- raise ValueError('Invalid date literal "{0}"'.format(value))
- datetime_from_iso8601.__schema__ = {"type": "string", "format": "date-time"}
- def date_from_iso8601(value):
- """
- Turns an ISO8601 formatted date into a date object.
- Example::
- inputs.date_from_iso8601("2012-01-01")
- :param str value: The ISO8601-complying string to transform
- :return: A date
- :rtype: date
- :raises ValueError: if value is an invalid date literal
- """
- return datetime_from_iso8601(value).date()
- date_from_iso8601.__schema__ = {"type": "string", "format": "date"}
|