|
| 1 | +import datetime |
| 2 | +import re |
| 3 | +import socket |
| 4 | + |
| 5 | +from jsonschema.compat import PY3 |
| 6 | + |
| 7 | + |
| 8 | +class FormatError(Exception): |
| 9 | + def __init__(self, message, cause=None): |
| 10 | + super(FormatError, self).__init__(message, cause) |
| 11 | + self.message = message |
| 12 | + self.cause = self.__cause__ = cause |
| 13 | + |
| 14 | + def __str__(self): |
| 15 | + return self.message.encode("utf-8") |
| 16 | + |
| 17 | + def __unicode__(self): |
| 18 | + return self.message |
| 19 | + |
| 20 | + if PY3: |
| 21 | + __str__ = __unicode__ |
| 22 | + |
| 23 | + |
| 24 | +class FormatChecker(object): |
| 25 | + """ |
| 26 | + A ``format`` property checker. |
| 27 | +
|
| 28 | + JSON Schema does not mandate that the ``format`` property actually do any |
| 29 | + validation. If validation is desired however, instances of this class can |
| 30 | + be hooked into validators to enable format validation. |
| 31 | +
|
| 32 | + :class:`FormatChecker` objects always return ``True`` when asked about |
| 33 | + formats that they do not know how to validate. |
| 34 | +
|
| 35 | + To check a custom format using a function that takes an instance and |
| 36 | + returns a ``bool``, use the :meth:`FormatChecker.checks` or |
| 37 | + :meth:`FormatChecker.cls_checks` decorators. |
| 38 | +
|
| 39 | + :argument iterable formats: the known formats to validate. This argument |
| 40 | + can be used to limit which formats will be used |
| 41 | + during validation. |
| 42 | +
|
| 43 | + """ |
| 44 | + |
| 45 | + checkers = {} |
| 46 | + |
| 47 | + def __init__(self, formats=None): |
| 48 | + if formats is None: |
| 49 | + self.checkers = self.checkers.copy() |
| 50 | + else: |
| 51 | + self.checkers = dict((k, self.checkers[k]) for k in formats) |
| 52 | + |
| 53 | + def checks(self, format, raises=()): |
| 54 | + """ |
| 55 | + Register a decorated function as validating a new format. |
| 56 | +
|
| 57 | + :argument str format: the format that the decorated function will check |
| 58 | + :argument Exception raises: the exception(s) raised by the decorated |
| 59 | + function when an invalid instance is found. The exception object |
| 60 | + will be accessible as the :attr:`ValidationError.cause` attribute |
| 61 | + of the resulting validation error. |
| 62 | +
|
| 63 | + """ |
| 64 | + |
| 65 | + def _checks(func): |
| 66 | + self.checkers[format] = (func, raises) |
| 67 | + return func |
| 68 | + return _checks |
| 69 | + |
| 70 | + cls_checks = classmethod(checks) |
| 71 | + |
| 72 | + def check(self, instance, format): |
| 73 | + """ |
| 74 | + Check whether the instance conforms to the given format. |
| 75 | +
|
| 76 | + :argument instance: the instance to check |
| 77 | + :type: any primitive type (str, number, bool) |
| 78 | + :argument str format: the format that instance should conform to |
| 79 | + :raises: :exc:`FormatError` if instance does not conform to format |
| 80 | +
|
| 81 | + """ |
| 82 | + |
| 83 | + if format in self.checkers: |
| 84 | + func, raises = self.checkers[format] |
| 85 | + result, cause = None, None |
| 86 | + try: |
| 87 | + result = func(instance) |
| 88 | + except raises as e: |
| 89 | + cause = e |
| 90 | + if not result: |
| 91 | + raise FormatError( |
| 92 | + "%r is not a %r" % (instance, format), cause=cause, |
| 93 | + ) |
| 94 | + |
| 95 | + def conforms(self, instance, format): |
| 96 | + """ |
| 97 | + Check whether the instance conforms to the given format. |
| 98 | +
|
| 99 | + :argument instance: the instance to check |
| 100 | + :type: any primitive type (str, number, bool) |
| 101 | + :argument str format: the format that instance should conform to |
| 102 | + :rtype: bool |
| 103 | +
|
| 104 | + """ |
| 105 | + |
| 106 | + try: |
| 107 | + self.check(instance, format) |
| 108 | + except FormatError: |
| 109 | + return False |
| 110 | + else: |
| 111 | + return True |
| 112 | + |
| 113 | + |
| 114 | +_draft_checkers = {"draft3": [], "draft4": []} |
| 115 | + |
| 116 | + |
| 117 | +def _checks_drafts(both=None, draft3=None, draft4=None, raises=()): |
| 118 | + draft3 = draft3 or both |
| 119 | + draft4 = draft4 or both |
| 120 | + |
| 121 | + def wrap(func): |
| 122 | + if draft3: |
| 123 | + _draft_checkers["draft3"].append(draft3) |
| 124 | + func = FormatChecker.cls_checks(draft3, raises)(func) |
| 125 | + if draft4: |
| 126 | + _draft_checkers["draft4"].append(draft4) |
| 127 | + func = FormatChecker.cls_checks(draft4, raises)(func) |
| 128 | + return func |
| 129 | + return wrap |
| 130 | + |
| 131 | + |
| 132 | +@_checks_drafts("email") |
| 133 | +def is_email(instance): |
| 134 | + return "@" in instance |
| 135 | + |
| 136 | + |
| 137 | +_checks_drafts(draft3="ip-address", draft4="ipv4", raises=socket.error)( |
| 138 | + socket.inet_aton |
| 139 | +) |
| 140 | + |
| 141 | + |
| 142 | +if hasattr(socket, "inet_pton"): |
| 143 | + @_checks_drafts("ipv6", raises=socket.error) |
| 144 | + def is_ipv6(instance): |
| 145 | + return socket.inet_pton(socket.AF_INET6, instance) |
| 146 | + |
| 147 | + |
| 148 | +@_checks_drafts(draft3="host-name", draft4="hostname") |
| 149 | +def is_host_name(instance): |
| 150 | + pattern = "^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$" |
| 151 | + if not re.match(pattern, instance): |
| 152 | + return False |
| 153 | + components = instance.split(".") |
| 154 | + for component in components: |
| 155 | + if len(component) > 63: |
| 156 | + return False |
| 157 | + return True |
| 158 | + |
| 159 | + |
| 160 | +try: |
| 161 | + import rfc3987 |
| 162 | +except ImportError: |
| 163 | + pass |
| 164 | +else: |
| 165 | + @_checks_drafts("uri", raises=ValueError) |
| 166 | + def is_uri(instance): |
| 167 | + return rfc3987.parse(instance, rule="URI_reference") |
| 168 | + |
| 169 | + |
| 170 | +try: |
| 171 | + import isodate |
| 172 | +except ImportError: |
| 173 | + pass |
| 174 | +else: |
| 175 | + _err = (ValueError, isodate.ISO8601Error) |
| 176 | + _checks_drafts("date-time", raises=_err)(isodate.parse_datetime) |
| 177 | + |
| 178 | + |
| 179 | +_checks_drafts("regex", raises=re.error)(re.compile) |
| 180 | + |
| 181 | + |
| 182 | +@_checks_drafts(draft3="date", raises=ValueError) |
| 183 | +def is_date(instance): |
| 184 | + return datetime.datetime.strptime(instance, "%Y-%m-%d") |
| 185 | + |
| 186 | + |
| 187 | +@_checks_drafts(draft3="time", raises=ValueError) |
| 188 | +def is_time(instance): |
| 189 | + return datetime.datetime.strptime(instance, "%H:%M:%S") |
| 190 | + |
| 191 | + |
| 192 | +try: |
| 193 | + import webcolors |
| 194 | +except ImportError: |
| 195 | + pass |
| 196 | +else: |
| 197 | + def is_css_color_code(instance): |
| 198 | + return webcolors.normalize_hex(instance) |
| 199 | + |
| 200 | + |
| 201 | + @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) |
| 202 | + def is_css21_color(instance): |
| 203 | + if instance.lower() in webcolors.css21_names_to_hex: |
| 204 | + return True |
| 205 | + return is_css_color_code(instance) |
| 206 | + |
| 207 | + |
| 208 | + def is_css3_color(instance): |
| 209 | + if instance.lower() in webcolors.css3_names_to_hex: |
| 210 | + return True |
| 211 | + return is_css_color_code(instance) |
| 212 | + |
| 213 | + |
| 214 | +draft3_format_checker = FormatChecker(_draft_checkers["draft3"]) |
| 215 | +draft4_format_checker = FormatChecker(_draft_checkers["draft4"]) |
0 commit comments