_format.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import datetime
  2. import re
  3. import socket
  4. import struct
  5. from jsonschema.compat import str_types
  6. from jsonschema.exceptions import FormatError
  7. class FormatChecker(object):
  8. """
  9. A ``format`` property checker.
  10. JSON Schema does not mandate that the ``format`` property actually do any
  11. validation. If validation is desired however, instances of this class can
  12. be hooked into validators to enable format validation.
  13. `FormatChecker` objects always return ``True`` when asked about
  14. formats that they do not know how to validate.
  15. To check a custom format using a function that takes an instance and
  16. returns a ``bool``, use the `FormatChecker.checks` or
  17. `FormatChecker.cls_checks` decorators.
  18. Arguments:
  19. formats (~collections.Iterable):
  20. The known formats to validate. This argument can be used to
  21. limit which formats will be used during validation.
  22. """
  23. checkers = {}
  24. def __init__(self, formats=None):
  25. if formats is None:
  26. self.checkers = self.checkers.copy()
  27. else:
  28. self.checkers = dict((k, self.checkers[k]) for k in formats)
  29. def __repr__(self):
  30. return "<FormatChecker checkers={}>".format(sorted(self.checkers))
  31. def checks(self, format, raises=()):
  32. """
  33. Register a decorated function as validating a new format.
  34. Arguments:
  35. format (str):
  36. The format that the decorated function will check.
  37. raises (Exception):
  38. The exception(s) raised by the decorated function when an
  39. invalid instance is found.
  40. The exception object will be accessible as the
  41. `jsonschema.exceptions.ValidationError.cause` attribute of the
  42. resulting validation error.
  43. """
  44. def _checks(func):
  45. self.checkers[format] = (func, raises)
  46. return func
  47. return _checks
  48. cls_checks = classmethod(checks)
  49. def check(self, instance, format):
  50. """
  51. Check whether the instance conforms to the given format.
  52. Arguments:
  53. instance (*any primitive type*, i.e. str, number, bool):
  54. The instance to check
  55. format (str):
  56. The format that instance should conform to
  57. Raises:
  58. FormatError: if the instance does not conform to ``format``
  59. """
  60. if format not in self.checkers:
  61. return
  62. func, raises = self.checkers[format]
  63. result, cause = None, None
  64. try:
  65. result = func(instance)
  66. except raises as e:
  67. cause = e
  68. if not result:
  69. raise FormatError(
  70. "%r is not a %r" % (instance, format), cause=cause,
  71. )
  72. def conforms(self, instance, format):
  73. """
  74. Check whether the instance conforms to the given format.
  75. Arguments:
  76. instance (*any primitive type*, i.e. str, number, bool):
  77. The instance to check
  78. format (str):
  79. The format that instance should conform to
  80. Returns:
  81. bool: whether it conformed
  82. """
  83. try:
  84. self.check(instance, format)
  85. except FormatError:
  86. return False
  87. else:
  88. return True
  89. draft3_format_checker = FormatChecker()
  90. draft4_format_checker = FormatChecker()
  91. draft6_format_checker = FormatChecker()
  92. draft7_format_checker = FormatChecker()
  93. _draft_checkers = dict(
  94. draft3=draft3_format_checker,
  95. draft4=draft4_format_checker,
  96. draft6=draft6_format_checker,
  97. draft7=draft7_format_checker,
  98. )
  99. def _checks_drafts(
  100. name=None,
  101. draft3=None,
  102. draft4=None,
  103. draft6=None,
  104. draft7=None,
  105. raises=(),
  106. ):
  107. draft3 = draft3 or name
  108. draft4 = draft4 or name
  109. draft6 = draft6 or name
  110. draft7 = draft7 or name
  111. def wrap(func):
  112. if draft3:
  113. func = _draft_checkers["draft3"].checks(draft3, raises)(func)
  114. if draft4:
  115. func = _draft_checkers["draft4"].checks(draft4, raises)(func)
  116. if draft6:
  117. func = _draft_checkers["draft6"].checks(draft6, raises)(func)
  118. if draft7:
  119. func = _draft_checkers["draft7"].checks(draft7, raises)(func)
  120. # Oy. This is bad global state, but relied upon for now, until
  121. # deprecation. See https://github.com/Julian/jsonschema/issues/519
  122. # and test_format_checkers_come_with_defaults
  123. FormatChecker.cls_checks(draft7 or draft6 or draft4 or draft3, raises)(
  124. func,
  125. )
  126. return func
  127. return wrap
  128. @_checks_drafts(name="idn-email")
  129. @_checks_drafts(name="email")
  130. def is_email(instance):
  131. if not isinstance(instance, str_types):
  132. return True
  133. return "@" in instance
  134. _ipv4_re = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
  135. @_checks_drafts(
  136. draft3="ip-address", draft4="ipv4", draft6="ipv4", draft7="ipv4",
  137. )
  138. def is_ipv4(instance):
  139. if not isinstance(instance, str_types):
  140. return True
  141. if not _ipv4_re.match(instance):
  142. return False
  143. return all(0 <= int(component) <= 255 for component in instance.split("."))
  144. if hasattr(socket, "inet_pton"):
  145. # FIXME: Really this only should raise struct.error, but see the sadness
  146. # that is https://twistedmatrix.com/trac/ticket/9409
  147. @_checks_drafts(
  148. name="ipv6", raises=(socket.error, struct.error, ValueError),
  149. )
  150. def is_ipv6(instance):
  151. if not isinstance(instance, str_types):
  152. return True
  153. return socket.inet_pton(socket.AF_INET6, instance)
  154. _host_name_re = re.compile(r"^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$")
  155. @_checks_drafts(
  156. draft3="host-name",
  157. draft4="hostname",
  158. draft6="hostname",
  159. draft7="hostname",
  160. )
  161. def is_host_name(instance):
  162. if not isinstance(instance, str_types):
  163. return True
  164. if not _host_name_re.match(instance):
  165. return False
  166. components = instance.split(".")
  167. for component in components:
  168. if len(component) > 63:
  169. return False
  170. return True
  171. try:
  172. # The built-in `idna` codec only implements RFC 3890, so we go elsewhere.
  173. import idna
  174. except ImportError:
  175. pass
  176. else:
  177. @_checks_drafts(draft7="idn-hostname", raises=idna.IDNAError)
  178. def is_idn_host_name(instance):
  179. if not isinstance(instance, str_types):
  180. return True
  181. idna.encode(instance)
  182. return True
  183. try:
  184. import rfc3987
  185. except ImportError:
  186. try:
  187. from rfc3986_validator import validate_rfc3986
  188. except ImportError:
  189. pass
  190. else:
  191. @_checks_drafts(name="uri")
  192. def is_uri(instance):
  193. if not isinstance(instance, str_types):
  194. return True
  195. return validate_rfc3986(instance, rule="URI")
  196. @_checks_drafts(
  197. draft6="uri-reference",
  198. draft7="uri-reference",
  199. raises=ValueError,
  200. )
  201. def is_uri_reference(instance):
  202. if not isinstance(instance, str_types):
  203. return True
  204. return validate_rfc3986(instance, rule="URI_reference")
  205. else:
  206. @_checks_drafts(draft7="iri", raises=ValueError)
  207. def is_iri(instance):
  208. if not isinstance(instance, str_types):
  209. return True
  210. return rfc3987.parse(instance, rule="IRI")
  211. @_checks_drafts(draft7="iri-reference", raises=ValueError)
  212. def is_iri_reference(instance):
  213. if not isinstance(instance, str_types):
  214. return True
  215. return rfc3987.parse(instance, rule="IRI_reference")
  216. @_checks_drafts(name="uri", raises=ValueError)
  217. def is_uri(instance):
  218. if not isinstance(instance, str_types):
  219. return True
  220. return rfc3987.parse(instance, rule="URI")
  221. @_checks_drafts(
  222. draft6="uri-reference",
  223. draft7="uri-reference",
  224. raises=ValueError,
  225. )
  226. def is_uri_reference(instance):
  227. if not isinstance(instance, str_types):
  228. return True
  229. return rfc3987.parse(instance, rule="URI_reference")
  230. try:
  231. from strict_rfc3339 import validate_rfc3339
  232. except ImportError:
  233. try:
  234. from rfc3339_validator import validate_rfc3339
  235. except ImportError:
  236. validate_rfc3339 = None
  237. if validate_rfc3339:
  238. @_checks_drafts(name="date-time")
  239. def is_datetime(instance):
  240. if not isinstance(instance, str_types):
  241. return True
  242. return validate_rfc3339(instance)
  243. @_checks_drafts(draft7="time")
  244. def is_time(instance):
  245. if not isinstance(instance, str_types):
  246. return True
  247. return is_datetime("1970-01-01T" + instance)
  248. @_checks_drafts(name="regex", raises=re.error)
  249. def is_regex(instance):
  250. if not isinstance(instance, str_types):
  251. return True
  252. return re.compile(instance)
  253. @_checks_drafts(draft3="date", draft7="date", raises=ValueError)
  254. def is_date(instance):
  255. if not isinstance(instance, str_types):
  256. return True
  257. return datetime.datetime.strptime(instance, "%Y-%m-%d")
  258. @_checks_drafts(draft3="time", raises=ValueError)
  259. def is_draft3_time(instance):
  260. if not isinstance(instance, str_types):
  261. return True
  262. return datetime.datetime.strptime(instance, "%H:%M:%S")
  263. try:
  264. import webcolors
  265. except ImportError:
  266. pass
  267. else:
  268. def is_css_color_code(instance):
  269. return webcolors.normalize_hex(instance)
  270. @_checks_drafts(draft3="color", raises=(ValueError, TypeError))
  271. def is_css21_color(instance):
  272. if (
  273. not isinstance(instance, str_types) or
  274. instance.lower() in webcolors.css21_names_to_hex
  275. ):
  276. return True
  277. return is_css_color_code(instance)
  278. def is_css3_color(instance):
  279. if instance.lower() in webcolors.css3_names_to_hex:
  280. return True
  281. return is_css_color_code(instance)
  282. try:
  283. import jsonpointer
  284. except ImportError:
  285. pass
  286. else:
  287. @_checks_drafts(
  288. draft6="json-pointer",
  289. draft7="json-pointer",
  290. raises=jsonpointer.JsonPointerException,
  291. )
  292. def is_json_pointer(instance):
  293. if not isinstance(instance, str_types):
  294. return True
  295. return jsonpointer.JsonPointer(instance)
  296. # TODO: I don't want to maintain this, so it
  297. # needs to go either into jsonpointer (pending
  298. # https://github.com/stefankoegl/python-json-pointer/issues/34) or
  299. # into a new external library.
  300. @_checks_drafts(
  301. draft7="relative-json-pointer",
  302. raises=jsonpointer.JsonPointerException,
  303. )
  304. def is_relative_json_pointer(instance):
  305. # Definition taken from:
  306. # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
  307. if not isinstance(instance, str_types):
  308. return True
  309. non_negative_integer, rest = [], ""
  310. for i, character in enumerate(instance):
  311. if character.isdigit():
  312. non_negative_integer.append(character)
  313. continue
  314. if not non_negative_integer:
  315. return False
  316. rest = instance[i:]
  317. break
  318. return (rest == "#") or jsonpointer.JsonPointer(rest)
  319. try:
  320. import uritemplate.exceptions
  321. except ImportError:
  322. pass
  323. else:
  324. @_checks_drafts(
  325. draft6="uri-template",
  326. draft7="uri-template",
  327. raises=uritemplate.exceptions.InvalidTemplate,
  328. )
  329. def is_uri_template(
  330. instance,
  331. template_validator=uritemplate.Validator().force_balanced_braces(),
  332. ):
  333. template = uritemplate.URITemplate(instance)
  334. return template_validator.validate(template)