summaryrefslogtreecommitdiff
path: root/jsonschema/__init__.py
diff options
context:
space:
mode:
authorColin Dunklau <colin.dunklau@gmail.com>2013-05-10 18:47:06 -0500
committerColin Dunklau <colin.dunklau@gmail.com>2013-05-10 18:47:06 -0500
commitfe450f221c74e117f572fd313081ab7165ebac26 (patch)
tree3c13cc9c906557d531f4dc9ebccf08d4f3462fa0 /jsonschema/__init__.py
parent806bda7f43264e23ff9e570b016aa52890bc1290 (diff)
downloadjsonschema-fe450f221c74e117f572fd313081ab7165ebac26.tar.gz
Began package split, done with compat and _utils
Diffstat (limited to 'jsonschema/__init__.py')
-rw-r--r--jsonschema/__init__.py1311
1 files changed, 1311 insertions, 0 deletions
diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py
new file mode 100644
index 0000000..ba99618
--- /dev/null
+++ b/jsonschema/__init__.py
@@ -0,0 +1,1311 @@
+"""
+An implementation of JSON Schema for Python
+
+The main functionality is provided by the validator classes for each of the
+supported JSON Schema versions.
+
+Most commonly, :func:`validate` is the quickest way to simply validate a given
+instance under a schema, and will create a validator for you.
+
+"""
+
+from __future__ import division, unicode_literals
+
+import collections
+import contextlib
+import datetime
+import itertools
+import json
+import numbers
+import operator
+import pprint
+import re
+import socket
+import sys
+import textwrap
+
+try:
+ import requests
+except ImportError:
+ requests = None
+
+__version__ = "1.4.0-dev"
+
+from .compat import (
+ PY3, MutableMapping, Sequence,
+ urlparse, unquote, urlopen,
+ basestring, unicode, long,
+ iteritems,
+)
+from ._utils import (
+ _indent, _format_as_index, _find_additional_properties, _extras_msg,
+ _types_msg, _flatten, _list, _unbool, _uniq,
+)
+
+
+FLOAT_TOLERANCE = 10 ** -15
+validators = {}
+
+
+class _Unset(object):
+ """
+ An as-of-yet unset attribute.
+
+ """
+
+ def __repr__(self):
+ return "<unset>"
+_unset = _Unset()
+
+
+class _Error(Exception):
+ def __init__(
+ self, message, validator=_unset, path=(), cause=None, context=(),
+ validator_value=_unset, instance=_unset, schema=_unset, schema_path=(),
+ ):
+ self.message = message
+ self.path = collections.deque(path)
+ self.schema_path = collections.deque(schema_path)
+ self.context = list(context)
+ self.cause = self.__cause__ = cause
+ self.validator = validator
+ self.validator_value = validator_value
+ self.instance = instance
+ self.schema = schema
+
+ @classmethod
+ def create_from(cls, other):
+ return cls(
+ message=other.message,
+ cause=other.cause,
+ context=other.context,
+ path=other.path,
+ schema_path=other.schema_path,
+ validator=other.validator,
+ validator_value=other.validator_value,
+ instance=other.instance,
+ schema=other.schema,
+ )
+
+ def _set(self, **kwargs):
+ for k, v in iteritems(kwargs):
+ if getattr(self, k) is _unset:
+ setattr(self, k, v)
+
+ def __repr__(self):
+ return "<%s: %r>" % (self.__class__.__name__, self.message)
+
+ def __str__(self):
+ return unicode(self).encode("utf-8")
+
+ def __unicode__(self):
+ if _unset in (
+ self.validator, self.validator_value, self.instance, self.schema,
+ ):
+ return self.message
+
+ path = _format_as_index(self.path)
+ schema_path = _format_as_index(list(self.schema_path)[:-1])
+
+ pschema = pprint.pformat(self.schema, width=72)
+ pinstance = pprint.pformat(self.instance, width=72)
+ return self.message + textwrap.dedent("""
+
+ Failed validating %r in schema%s:
+ %s
+
+ On instance%s:
+ %s
+ """.rstrip()
+ ) % (
+ self.validator,
+ schema_path,
+ _indent(pschema),
+ path,
+ _indent(pinstance),
+ )
+
+ if PY3:
+ __str__ = __unicode__
+
+
+class FormatError(Exception):
+ def __init__(self, message, cause=None):
+ super(FormatError, self).__init__(message, cause)
+ self.message = message
+ self.cause = self.__cause__ = cause
+
+ def __str__(self):
+ return self.message.encode("utf-8")
+
+ def __unicode__(self):
+ return self.message
+
+ if PY3:
+ __str__ = __unicode__
+
+
+class SchemaError(_Error): pass
+class ValidationError(_Error): pass
+class RefResolutionError(Exception): pass
+class UnknownType(Exception): pass
+
+
+class _URIDict(MutableMapping):
+ """
+ Dictionary which uses normalized URIs as keys.
+
+ """
+
+ def normalize(self, uri):
+ return urlparse.urlsplit(uri).geturl()
+
+ def __init__(self, *args, **kwargs):
+ self.store = dict()
+ self.store.update(*args, **kwargs)
+
+ def __getitem__(self, uri):
+ return self.store[self.normalize(uri)]
+
+ def __setitem__(self, uri, value):
+ self.store[self.normalize(uri)] = value
+
+ def __delitem__(self, uri):
+ del self.store[self.normalize(uri)]
+
+ def __iter__(self):
+ return iter(self.store)
+
+ def __len__(self):
+ return len(self.store)
+
+ def __repr__(self):
+ return repr(self.store)
+
+
+meta_schemas = _URIDict()
+
+
+def validates(version):
+ """
+ Register the decorated validator for a ``version`` of the specification.
+
+ Registered validators and their meta schemas will be considered when
+ parsing ``$schema`` properties' URIs.
+
+ :argument str version: an identifier to use as the version's name
+ :returns: a class decorator to decorate the validator with the version
+
+ """
+
+ def _validates(cls):
+ validators[version] = cls
+ if "id" in cls.META_SCHEMA:
+ meta_schemas[cls.META_SCHEMA["id"]] = cls
+ return cls
+ return _validates
+
+
+class ValidatorMixin(object):
+ """
+ Concrete implementation of :class:`IValidator`.
+
+ Provides default implementations of each method. Validation of schema
+ properties is dispatched to ``validate_property`` methods. E.g., to
+ implement a validator for a ``maximum`` property, create a
+ ``validate_maximum`` method. Validator methods should yield zero or more
+ :exc:`ValidationError``\s to signal failed validation.
+
+ """
+
+ DEFAULT_TYPES = {
+ "array" : list, "boolean" : bool, "integer" : (int, long),
+ "null" : type(None), "number" : numbers.Number, "object" : dict,
+ "string" : basestring,
+ }
+
+ def __init__(self, schema, types=(), resolver=None, format_checker=None):
+ self._types = dict(self.DEFAULT_TYPES)
+ self._types.update(types)
+
+ if resolver is None:
+ resolver = RefResolver.from_schema(schema)
+
+ self.resolver = resolver
+ self.format_checker = format_checker
+ self.schema = schema
+
+ def is_type(self, instance, type):
+ if type not in self._types:
+ raise UnknownType(type)
+ pytypes = self._types[type]
+
+ # bool inherits from int, so ensure bools aren't reported as integers
+ if isinstance(instance, bool):
+ pytypes = _flatten(pytypes)
+ num = any(issubclass(pytype, numbers.Number) for pytype in pytypes)
+ if num and bool not in pytypes:
+ return False
+ return isinstance(instance, pytypes)
+
+ def is_valid(self, instance, _schema=None):
+ error = next(self.iter_errors(instance, _schema), None)
+ return error is None
+
+ @classmethod
+ def check_schema(cls, schema):
+ for error in cls(cls.META_SCHEMA).iter_errors(schema):
+ raise SchemaError.create_from(error)
+
+ def iter_errors(self, instance, _schema=None):
+ if _schema is None:
+ _schema = self.schema
+
+ with self.resolver.in_scope(_schema.get("id", "")):
+ ref = _schema.get("$ref")
+ if ref is not None:
+ validators = [("$ref", ref)]
+ else:
+ validators = iteritems(_schema)
+
+ for k, v in validators:
+ validator_attr = "validate_%s" % (k.lstrip("$"),)
+ validator = getattr(self, validator_attr, None)
+
+ if validator is None:
+ continue
+
+ errors = validator(v, instance, _schema) or ()
+ for error in errors:
+ # set details if they weren't already set by the called fn
+ error._set(
+ validator=k,
+ validator_value=v,
+ instance=instance,
+ schema=_schema,
+ )
+ if k != "$ref":
+ error.schema_path.appendleft(k)
+ yield error
+
+ def descend(self, instance, schema, path=None, schema_path=None):
+ for error in self.iter_errors(instance, schema):
+ if path is not None:
+ error.path.appendleft(path)
+ if schema_path is not None:
+ error.schema_path.appendleft(schema_path)
+ yield error
+
+ def validate(self, *args, **kwargs):
+ for error in self.iter_errors(*args, **kwargs):
+ raise error
+
+
+class _Draft34CommonMixin(object):
+ """
+ Contains the validator methods common to both JSON schema drafts.
+
+ """
+
+ def validate_patternProperties(self, patternProperties, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+
+ for pattern, subschema in iteritems(patternProperties):
+ for k, v in iteritems(instance):
+ if re.search(pattern, k):
+ for error in self.descend(
+ v, subschema, path=k, schema_path=pattern
+ ):
+ yield error
+
+ def validate_additionalProperties(self, aP, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+
+ extras = set(_find_additional_properties(instance, schema))
+
+ if self.is_type(aP, "object"):
+ for extra in extras:
+ for error in self.descend(instance[extra], aP, path=extra):
+ yield error
+ elif not aP and extras:
+ error = "Additional properties are not allowed (%s %s unexpected)"
+ yield ValidationError(error % _extras_msg(extras))
+
+ def validate_items(self, items, instance, schema):
+ if not self.is_type(instance, "array"):
+ return
+
+ if self.is_type(items, "object"):
+ for index, item in enumerate(instance):
+ for error in self.descend(item, items, path=index):
+ yield error
+ else:
+ for (index, item), subschema in zip(enumerate(instance), items):
+ for error in self.descend(
+ item, subschema, path=index, schema_path=index
+ ):
+ yield error
+
+ def validate_additionalItems(self, aI, instance, schema):
+ if (
+ not self.is_type(instance, "array") or
+ self.is_type(schema.get("items", {}), "object")
+ ):
+ return
+
+ if self.is_type(aI, "object"):
+ for index, item in enumerate(
+ instance[len(schema.get("items", [])):]):
+ for error in self.descend(item, aI, path=index):
+ yield error
+ elif not aI and len(instance) > len(schema.get("items", [])):
+ error = "Additional items are not allowed (%s %s unexpected)"
+ yield ValidationError(
+ error % _extras_msg(instance[len(schema.get("items", [])):])
+ )
+
+ def validate_minimum(self, minimum, instance, schema):
+ if not self.is_type(instance, "number"):
+ return
+
+ instance = float(instance)
+ if schema.get("exclusiveMinimum", False):
+ failed = instance <= minimum
+ cmp = "less than or equal to"
+ else:
+ failed = instance < minimum
+ cmp = "less than"
+
+ if failed:
+ yield ValidationError(
+ "%r is %s the minimum of %r" % (instance, cmp, minimum)
+ )
+
+ def validate_maximum(self, maximum, instance, schema):
+ if not self.is_type(instance, "number"):
+ return
+
+ instance = float(instance)
+ if schema.get("exclusiveMaximum", False):
+ failed = instance >= maximum
+ cmp = "greater than or equal to"
+ else:
+ failed = instance > maximum
+ cmp = "greater than"
+
+ if failed:
+ yield ValidationError(
+ "%r is %s the maximum of %r" % (instance, cmp, maximum)
+ )
+
+ def _validate_multipleOf(self, dB, instance, schema):
+ if not self.is_type(instance, "number"):
+ return
+
+ if isinstance(dB, float):
+ mod = instance % dB
+ failed = (mod > FLOAT_TOLERANCE) and (dB - mod) > FLOAT_TOLERANCE
+ else:
+ failed = instance % dB
+
+ if failed:
+ yield ValidationError(
+ "%r is not a multiple of %r" % (instance, dB)
+ )
+
+ def validate_minItems(self, mI, instance, schema):
+ if self.is_type(instance, "array") and len(instance) < mI:
+ yield ValidationError("%r is too short" % (instance,))
+
+ def validate_maxItems(self, mI, instance, schema):
+ if self.is_type(instance, "array") and len(instance) > mI:
+ yield ValidationError("%r is too long" % (instance,))
+
+ def validate_uniqueItems(self, uI, instance, schema):
+ if uI and self.is_type(instance, "array") and not _uniq(instance):
+ yield ValidationError("%r has non-unique elements" % instance)
+
+ def validate_pattern(self, patrn, instance, schema):
+ if self.is_type(instance, "string") and not re.search(patrn, instance):
+ yield ValidationError("%r does not match %r" % (instance, patrn))
+
+ def validate_format(self, format, instance, schema):
+ if (
+ self.format_checker is not None and
+ self.is_type(instance, "string")
+ ):
+ try:
+ self.format_checker.check(instance, format)
+ except FormatError as error:
+ yield ValidationError(error.message, cause=error.cause)
+
+ def validate_minLength(self, mL, instance, schema):
+ if self.is_type(instance, "string") and len(instance) < mL:
+ yield ValidationError("%r is too short" % (instance,))
+
+ def validate_maxLength(self, mL, instance, schema):
+ if self.is_type(instance, "string") and len(instance) > mL:
+ yield ValidationError("%r is too long" % (instance,))
+
+ def validate_dependencies(self, dependencies, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+
+ for property, dependency in iteritems(dependencies):
+ if property not in instance:
+ continue
+
+ if self.is_type(dependency, "object"):
+ for error in self.descend(
+ instance, dependency, schema_path=property
+ ):
+ yield error
+ else:
+ dependencies = _list(dependency)
+ for dependency in dependencies:
+ if dependency not in instance:
+ yield ValidationError(
+ "%r is a dependency of %r" % (dependency, property)
+ )
+
+ def validate_enum(self, enums, instance, schema):
+ if instance not in enums:
+ yield ValidationError("%r is not one of %r" % (instance, enums))
+
+ def validate_ref(self, ref, instance, schema):
+ with self.resolver.resolving(ref) as resolved:
+ for error in self.descend(instance, resolved):
+ yield error
+
+
+@validates("draft3")
+class Draft3Validator(ValidatorMixin, _Draft34CommonMixin, object):
+ """
+ A validator for JSON Schema draft 3.
+
+ """
+
+ def validate_type(self, types, instance, schema):
+ types = _list(types)
+
+ all_errors = []
+ for index, type in enumerate(types):
+ if type == "any":
+ return
+ if self.is_type(type, "object"):
+ errors = list(self.descend(instance, type, schema_path=index))
+ if not errors:
+ return
+ all_errors.extend(errors)
+ elif self.is_type(type, "string"):
+ if self.is_type(instance, type):
+ return
+ else:
+ yield ValidationError(
+ _types_msg(instance, types), context=all_errors,
+ )
+
+ def validate_properties(self, properties, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+
+ for property, subschema in iteritems(properties):
+ if property in instance:
+ for error in self.descend(
+ instance[property],
+ subschema,
+ path=property,
+ schema_path=property,
+ ):
+ yield error
+ elif subschema.get("required", False):
+ error = ValidationError("%r is a required property" % property)
+ error._set(
+ validator="required",
+ validator_value=subschema["required"],
+ instance=instance,
+ schema=schema,
+ )
+ error.path.appendleft(property)
+ error.schema_path.extend([property, "required"])
+ yield error
+
+ def validate_disallow(self, disallow, instance, schema):
+ for disallowed in _list(disallow):
+ if self.is_valid(instance, {"type" : [disallowed]}):
+ yield ValidationError(
+ "%r is disallowed for %r" % (disallowed, instance)
+ )
+
+ def validate_extends(self, extends, instance, schema):
+ if self.is_type(extends, "object"):
+ for error in self.descend(instance, extends):
+ yield error
+ return
+ for index, subschema in enumerate(extends):
+ for error in self.descend(instance, subschema, schema_path=index):
+ yield error
+
+ validate_divisibleBy = _Draft34CommonMixin._validate_multipleOf
+
+ META_SCHEMA = {
+ "$schema" : "http://json-schema.org/draft-03/schema#",
+ "id" : "http://json-schema.org/draft-03/schema#",
+ "type" : "object",
+
+ "properties" : {
+ "type" : {
+ "type" : ["string", "array"],
+ "items" : {"type" : ["string", {"$ref" : "#"}]},
+ "uniqueItems" : True,
+ "default" : "any"
+ },
+ "properties" : {
+ "type" : "object",
+ "additionalProperties" : {"$ref" : "#", "type": "object"},
+ "default" : {}
+ },
+ "patternProperties" : {
+ "type" : "object",
+ "additionalProperties" : {"$ref" : "#"},
+ "default" : {}
+ },
+ "additionalProperties" : {
+ "type" : [{"$ref" : "#"}, "boolean"], "default" : {}
+ },
+ "items" : {
+ "type" : [{"$ref" : "#"}, "array"],
+ "items" : {"$ref" : "#"},
+ "default" : {}
+ },
+ "additionalItems" : {
+ "type" : [{"$ref" : "#"}, "boolean"], "default" : {}
+ },
+ "required" : {"type" : "boolean", "default" : False},
+ "dependencies" : {
+ "type" : ["string", "array", "object"],
+ "additionalProperties" : {
+ "type" : ["string", "array", {"$ref" : "#"}],
+ "items" : {"type" : "string"}
+ },
+ "default" : {}
+ },
+ "minimum" : {"type" : "number"},
+ "maximum" : {"type" : "number"},
+ "exclusiveMinimum" : {"type" : "boolean", "default" : False},
+ "exclusiveMaximum" : {"type" : "boolean", "default" : False},
+ "minItems" : {"type" : "integer", "minimum" : 0, "default" : 0},
+ "maxItems" : {"type" : "integer", "minimum" : 0},
+ "uniqueItems" : {"type" : "boolean", "default" : False},
+ "pattern" : {"type" : "string", "format" : "regex"},
+ "minLength" : {"type" : "integer", "minimum" : 0, "default" : 0},
+ "maxLength" : {"type" : "integer"},
+ "enum" : {"type" : "array", "minItems" : 1, "uniqueItems" : True},
+ "default" : {"type" : "any"},
+ "title" : {"type" : "string"},
+ "description" : {"type" : "string"},
+ "format" : {"type" : "string"},
+ "maxDecimal" : {"type" : "number", "minimum" : 0},
+ "divisibleBy" : {
+ "type" : "number",
+ "minimum" : 0,
+ "exclusiveMinimum" : True,
+ "default" : 1
+ },
+ "disallow" : {
+ "type" : ["string", "array"],
+ "items" : {"type" : ["string", {"$ref" : "#"}]},
+ "uniqueItems" : True
+ },
+ "extends" : {
+ "type" : [{"$ref" : "#"}, "array"],
+ "items" : {"$ref" : "#"},
+ "default" : {}
+ },
+ "id" : {"type" : "string", "format" : "uri"},
+ "$ref" : {"type" : "string", "format" : "uri"},
+ "$schema" : {"type" : "string", "format" : "uri"},
+ },
+ "dependencies" : {
+ "exclusiveMinimum" : "minimum", "exclusiveMaximum" : "maximum"
+ },
+ }
+
+
+@validates("draft4")
+class Draft4Validator(ValidatorMixin, _Draft34CommonMixin, object):
+ """
+ A validator for JSON Schema draft 4.
+
+ """
+
+ def validate_type(self, types, instance, schema):
+ types = _list(types)
+
+ if not any(self.is_type(instance, type) for type in types):
+ yield ValidationError(_types_msg(instance, types))
+
+ def validate_properties(self, properties, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+
+ for property, subschema in iteritems(properties):
+ if property in instance:
+ for error in self.descend(
+ instance[property],
+ subschema,
+ path=property,
+ schema_path=property,
+ ):
+ yield error
+
+ def validate_required(self, required, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+ for property in required:
+ if property not in instance:
+ yield ValidationError("%r is a required property" % property)
+
+ def validate_minProperties(self, mP, instance, schema):
+ if self.is_type(instance, "object") and len(instance) < mP:
+ yield ValidationError("%r is too short" % (instance,))
+
+ def validate_maxProperties(self, mP, instance, schema):
+ if not self.is_type(instance, "object"):
+ return
+ if self.is_type(instance, "object") and len(instance) > mP:
+ yield ValidationError("%r is too short" % (instance,))
+
+ def validate_allOf(self, allOf, instance, schema):
+ for index, subschema in enumerate(allOf):
+ for error in self.descend(instance, subschema, schema_path=index):
+ yield error
+
+ def validate_oneOf(self, oneOf, instance, schema):
+ subschemas = enumerate(oneOf)
+ all_errors = []
+ for index, subschema in subschemas:
+ errors = list(self.descend(instance, subschema, schema_path=index))
+ if not errors:
+ first_valid = subschema
+ break
+ all_errors.extend(errors)
+ else:
+ yield ValidationError(
+ "%r is not valid under any of the given schemas" % (instance,),
+ context=all_errors,
+ )
+
+ more_valid = [s for i, s in subschemas if self.is_valid(instance, s)]
+ if more_valid:
+ more_valid.append(first_valid)
+ reprs = ", ".join(repr(schema) for schema in more_valid)
+ yield ValidationError(
+ "%r is valid under each of %s" % (instance, reprs)
+ )
+
+ def validate_anyOf(self, anyOf, instance, schema):
+ all_errors = []
+ for index, subschema in enumerate(anyOf):
+ errors = list(self.descend(instance, subschema, schema_path=index))
+ if not errors:
+ break
+ all_errors.extend(errors)
+ else:
+ yield ValidationError(
+ "%r is not valid under any of the given schemas" % (instance,),
+ context=all_errors,
+ )
+
+ def validate_not(self, not_schema, instance, schema):
+ if self.is_valid(instance, not_schema):
+ yield ValidationError(
+ "%r is not allowed for %r" % (not_schema, instance)
+ )
+
+ validate_multipleOf = _Draft34CommonMixin._validate_multipleOf
+
+ META_SCHEMA = {
+ "id": "http://json-schema.org/draft-04/schema#",
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Core schema meta-schema",
+ "definitions": {
+ "schemaArray": {
+ "type": "array",
+ "minItems": 1,
+ "items": {"$ref": "#"}
+ },
+ "positiveInteger": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [
+ {"$ref": "#/definitions/positiveInteger"}, {"default": 0}
+ ]
+ },
+ "simpleTypes": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "null",
+ "number",
+ "object",
+ "string",
+ ]
+ },
+ "stringArray": {
+ "type": "array",
+ "items": {"type": "string"},
+ "minItems": 1,
+ "uniqueItems": True
+ }
+ },
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uri"
+ },
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "default": {},
+ "multipleOf": {
+ "type": "number",
+ "minimum": 0,
+ "exclusiveMinimum": True
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "exclusiveMaximum": {
+ "type": "boolean",
+ "default": False
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "exclusiveMinimum": {
+ "type": "boolean",
+ "default": False
+ },
+ "maxLength": {"$ref": "#/definitions/positiveInteger"},
+ "minLength": {"$ref": "#/definitions/positiveIntegerDefault0"},
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "additionalItems": {
+ "anyOf": [
+ {"type": "boolean"},
+ {"$ref": "#"}
+ ],
+ "default": {}
+ },
+ "items": {
+ "anyOf": [
+ {"$ref": "#"},
+ {"$ref": "#/definitions/schemaArray"}
+ ],
+ "default": {}
+ },
+ "maxItems": {"$ref": "#/definitions/positiveInteger"},
+ "minItems": {"$ref": "#/definitions/positiveIntegerDefault0"},
+ "uniqueItems": {
+ "type": "boolean",
+ "default": False
+ },
+ "maxProperties": {"$ref": "#/definitions/positiveInteger"},
+ "minProperties": {"$ref": "#/definitions/positiveIntegerDefault0"},
+ "required": {"$ref": "#/definitions/stringArray"},
+ "additionalProperties": {
+ "anyOf": [
+ {"type": "boolean"},
+ {"$ref": "#"}
+ ],
+ "default": {}
+ },
+ "definitions": {
+ "type": "object",
+ "additionalProperties": {"$ref": "#"},
+ "default": {}
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": {"$ref": "#"},
+ "default": {}
+ },
+ "patternProperties": {
+ "type": "object",
+ "additionalProperties": {"$ref": "#"},
+ "default": {}
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {"$ref": "#"},
+ {"$ref": "#/definitions/stringArray"}
+ ]
+ }
+ },
+ "enum": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": True
+ },
+ "type": {
+ "anyOf": [
+ {"$ref": "#/definitions/simpleTypes"},
+ {
+ "type": "array",
+ "items": {"$ref": "#/definitions/simpleTypes"},
+ "minItems": 1,
+ "uniqueItems": True
+ }
+ ]
+ },
+ "allOf": {"$ref": "#/definitions/schemaArray"},
+ "anyOf": {"$ref": "#/definitions/schemaArray"},
+ "oneOf": {"$ref": "#/definitions/schemaArray"},
+ "not": {"$ref": "#"}
+ },
+ "dependencies": {
+ "exclusiveMaximum": ["maximum"],
+ "exclusiveMinimum": ["minimum"]
+ },
+ "default": {}
+ }
+
+
+class FormatChecker(object):
+ """
+ A ``format`` property checker.
+
+ JSON Schema does not mandate that the ``format`` property actually do any
+ validation. If validation is desired however, instances of this class can
+ be hooked into validators to enable format validation.
+
+ :class:`FormatChecker` objects always return ``True`` when asked about
+ formats that they do not know how to validate.
+
+ To check a custom format using a function that takes an instance and
+ returns a ``bool``, use the :meth:`FormatChecker.checks` or
+ :meth:`FormatChecker.cls_checks` decorators.
+
+ :argument iterable formats: the known formats to validate. This argument
+ can be used to limit which formats will be used
+ during validation.
+
+ >>> checker = FormatChecker(formats=("date-time", "regex"))
+
+ """
+
+ checkers = {}
+
+ def __init__(self, formats=None):
+ if formats is None:
+ self.checkers = self.checkers.copy()
+ else:
+ self.checkers = dict((k, self.checkers[k]) for k in formats)
+
+ def checks(self, format, raises=()):
+ """
+ Register a decorated function as validating a new format.
+
+ :argument str format: the format that the decorated function will check
+ :argument Exception raises: the exception(s) raised by the decorated
+ function when an invalid instance is found. The exception object
+ will be accessible as the :attr:`ValidationError.cause` attribute
+ of the resulting validation error.
+
+ """
+
+ def _checks(func):
+ self.checkers[format] = (func, raises)
+ return func
+ return _checks
+
+ cls_checks = classmethod(checks)
+
+ def check(self, instance, format):
+ """
+ Check whether the instance conforms to the given format.
+
+ :argument instance: the instance to check
+ :type: any primitive type (str, number, bool)
+ :argument str format: the format that instance should conform to
+ :raises: :exc:`FormatError` if instance does not conform to format
+
+ """
+
+ if format in self.checkers:
+ func, raises = self.checkers[format]
+ result, cause = None, None
+ try:
+ result = func(instance)
+ except raises as e:
+ cause = e
+ if not result:
+ raise FormatError(
+ "%r is not a %r" % (instance, format), cause=cause,
+ )
+
+ def conforms(self, instance, format):
+ """
+ Check whether the instance conforms to the given format.
+
+ :argument instance: the instance to check
+ :type: any primitive type (str, number, bool)
+ :argument str format: the format that instance should conform to
+ :rtype: bool
+
+ """
+
+ try:
+ self.check(instance, format)
+ except FormatError:
+ return False
+ else:
+ return True
+
+
+_draft_checkers = {"draft3": [], "draft4": []}
+
+
+def _checks_drafts(both=None, draft3=None, draft4=None, raises=()):
+ draft3 = draft3 or both
+ draft4 = draft4 or both
+
+ def wrap(func):
+ if draft3:
+ _draft_checkers["draft3"].append(draft3)
+ func = FormatChecker.cls_checks(draft3, raises)(func)
+ if draft4:
+ _draft_checkers["draft4"].append(draft4)
+ func = FormatChecker.cls_checks(draft4, raises)(func)
+ return func
+ return wrap
+
+
+@_checks_drafts("email")
+def is_email(instance):
+ return "@" in instance
+
+
+_checks_drafts(draft3="ip-address", draft4="ipv4", raises=socket.error)(
+ socket.inet_aton
+)
+
+
+if hasattr(socket, "inet_pton"):
+ @_checks_drafts("ipv6", raises=socket.error)
+ def is_ipv6(instance):
+ return socket.inet_pton(socket.AF_INET6, instance)
+
+
+@_checks_drafts(draft3="host-name", draft4="hostname")
+def is_host_name(instance):
+ pattern = "^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$"
+ if not re.match(pattern, instance):
+ return False
+ components = instance.split(".")
+ for component in components:
+ if len(component) > 63:
+ return False
+ return True
+
+
+try:
+ import rfc3987
+except ImportError:
+ pass
+else:
+ @_checks_drafts("uri", raises=ValueError)
+ def is_uri(instance):
+ return rfc3987.parse(instance, rule="URI_reference")
+
+
+try:
+ import isodate
+except ImportError:
+ pass
+else:
+ _err = (ValueError, isodate.ISO8601Error)
+ _checks_drafts("date-time", raises=_err)(isodate.parse_datetime)
+
+
+_checks_drafts("regex", raises=re.error)(re.compile)
+
+
+@_checks_drafts(draft3="date", raises=ValueError)
+def is_date(instance):
+ return datetime.datetime.strptime(instance, "%Y-%m-%d")
+
+
+@_checks_drafts(draft3="time", raises=ValueError)
+def is_time(instance):
+ return datetime.datetime.strptime(instance, "%H:%M:%S")
+
+
+try:
+ import webcolors
+except ImportError:
+ pass
+else:
+ def is_css_color_code(instance):
+ return webcolors.normalize_hex(instance)
+
+
+ @_checks_drafts(draft3="color", raises=(ValueError, TypeError))
+ def is_css21_color(instance):
+ if instance.lower() in webcolors.css21_names_to_hex:
+ return True
+ return is_css_color_code(instance)
+
+
+ def is_css3_color(instance):
+ if instance.lower() in webcolors.css3_names_to_hex:
+ return True
+ return is_css_color_code(instance)
+
+
+draft3_format_checker = FormatChecker(_draft_checkers["draft3"])
+draft4_format_checker = FormatChecker(_draft_checkers["draft4"])
+
+
+class RefResolver(object):
+ """
+ Resolve JSON References.
+
+ :argument str base_uri: URI of the referring document
+ :argument referrer: the actual referring document
+ :argument dict store: a mapping from URIs to documents to cache
+ :argument bool cache_remote: whether remote refs should be cached after
+ first resolution
+ :argument dict handlers: a mapping from URI schemes to functions that
+ should be used to retrieve them
+
+ """
+
+ def __init__(
+ self, base_uri, referrer, store=(), cache_remote=True, handlers=(),
+ ):
+ self.base_uri = base_uri
+ self.resolution_scope = base_uri
+ self.referrer = referrer
+ self.cache_remote = cache_remote
+ self.handlers = dict(handlers)
+
+ self.store = _URIDict(
+ (id, validator.META_SCHEMA)
+ for id, validator in iteritems(meta_schemas)
+ )
+ self.store.update(store)
+
+ @classmethod
+ def from_schema(cls, schema, *args, **kwargs):
+ """
+ Construct a resolver from a JSON schema object.
+
+ :argument schema schema: the referring schema
+ :rtype: :class:`RefResolver`
+
+ """
+
+ return cls(schema.get("id", ""), schema, *args, **kwargs)
+
+ @contextlib.contextmanager
+ def in_scope(self, scope):
+ old_scope = self.resolution_scope
+ self.resolution_scope = urlparse.urljoin(old_scope, scope)
+ try:
+ yield
+ finally:
+ self.resolution_scope = old_scope
+
+ @contextlib.contextmanager
+ def resolving(self, ref):
+ """
+ Context manager which resolves a JSON ``ref`` and enters the
+ resolution scope of this ref.
+
+ :argument str ref: reference to resolve
+
+ """
+
+ full_uri = urlparse.urljoin(self.resolution_scope, ref)
+ uri, fragment = urlparse.urldefrag(full_uri)
+
+ if uri in self.store:
+ document = self.store[uri]
+ elif not uri or uri == self.base_uri:
+ document = self.referrer
+ else:
+ try:
+ document = self.resolve_remote(uri)
+ except Exception as exc:
+ raise RefResolutionError(exc)
+
+ old_base_uri, old_referrer = self.base_uri, self.referrer
+ self.base_uri, self.referrer = uri, document
+ try:
+ with self.in_scope(uri):
+ yield self.resolve_fragment(document, fragment)
+ finally:
+ self.base_uri, self.referrer = old_base_uri, old_referrer
+
+ def resolve_fragment(self, document, fragment):
+ """
+ Resolve a ``fragment`` within the referenced ``document``.
+
+ :argument document: the referrant document
+ :argument str fragment: a URI fragment to resolve within it
+
+ """
+
+ fragment = fragment.lstrip("/")
+ parts = unquote(fragment).split("/") if fragment else []
+
+ for part in parts:
+ part = part.replace("~1", "/").replace("~0", "~")
+
+ if isinstance(document, Sequence):
+ # Array indexes should be turned into integers
+ try:
+ part = int(part)
+ except ValueError:
+ pass
+ try:
+ document = document[part]
+ except (TypeError, LookupError):
+ raise RefResolutionError(
+ "Unresolvable JSON pointer: %r" % fragment
+ )
+
+ return document
+
+ def resolve_remote(self, uri):
+ """
+ Resolve a remote ``uri``.
+
+ Does not check the store first, but stores the retrieved document in
+ the store if :attr:`RefResolver.cache_remote` is True.
+
+ .. note::
+
+ If the requests_ library is present, ``jsonschema`` will use it to
+ request the remote ``uri``, so that the correct encoding is
+ detected and used.
+
+ If it isn't, or if the scheme of the ``uri`` is not ``http`` or
+ ``https``, UTF-8 is assumed.
+
+ :argument str uri: the URI to resolve
+ :returns: the retrieved document
+
+ .. _requests: http://pypi.python.org/pypi/requests/
+
+ """
+
+ scheme = urlparse.urlsplit(uri).scheme
+
+ if scheme in self.handlers:
+ result = self.handlers[scheme](uri)
+ elif (
+ scheme in ["http", "https"] and
+ requests and
+ getattr(requests.Response, "json", None) is not None
+ ):
+ # Requests has support for detecting the correct encoding of
+ # json over http
+ if callable(requests.Response.json):
+ result = requests.get(uri).json()
+ else:
+ result = requests.get(uri).json
+ else:
+ # Otherwise, pass off to urllib and assume utf-8
+ result = json.loads(urlopen(uri).read().decode("utf-8"))
+
+ if self.cache_remote:
+ self.store[uri] = result
+ return result
+
+
+class ErrorTree(object):
+ """
+ ErrorTrees make it easier to check which validations failed.
+
+ """
+
+ _instance = _unset
+
+ def __init__(self, errors=()):
+ self.errors = {}
+ self._contents = collections.defaultdict(self.__class__)
+
+ for error in errors:
+ container = self
+ for element in error.path:
+ container = container[element]
+ container.errors[error.validator] = error
+
+ self._instance = error.instance
+
+ def __contains__(self, k):
+ return k in self._contents
+
+ def __getitem__(self, k):
+ """
+ Retrieve the child tree with key ``k``.
+
+ If the key is not in the instance that this tree corresponds to and is
+ not known by this tree, whatever error would be raised by
+ ``instance.__getitem__`` will be propagated (usually this is some
+ subclass of :class:`LookupError`.
+
+ """
+
+ if self._instance is not _unset and k not in self:
+ self._instance[k]
+ return self._contents[k]
+
+ def __setitem__(self, k, v):
+ self._contents[k] = v
+
+ def __iter__(self):
+ return iter(self._contents)
+
+ def __len__(self):
+ return self.total_errors
+
+ def __repr__(self):
+ return "<%s (%s total errors)>" % (self.__class__.__name__, len(self))
+
+ @property
+ def total_errors(self):
+ """
+ The total number of errors in the entire tree, including children.
+
+ """
+
+ child_errors = sum(len(tree) for _, tree in iteritems(self._contents))
+ return len(self.errors) + child_errors
+
+
+def validate(instance, schema, cls=None, *args, **kwargs):
+ if cls is None:
+ cls = meta_schemas.get(schema.get("$schema", ""), Draft4Validator)
+ cls.check_schema(schema)
+ cls(schema, *args, **kwargs).validate(instance)