diff options
Diffstat (limited to 'src/leap/base/jsonschema.py')
-rw-r--r-- | src/leap/base/jsonschema.py | 791 |
1 files changed, 0 insertions, 791 deletions
diff --git a/src/leap/base/jsonschema.py b/src/leap/base/jsonschema.py deleted file mode 100644 index 56689b08..00000000 --- a/src/leap/base/jsonschema.py +++ /dev/null @@ -1,791 +0,0 @@ -""" -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 json -import itertools -import operator -import re -import sys - - -__version__ = "0.8.0" - -PY3 = sys.version_info[0] >= 3 - -if PY3: - from urllib import parse as urlparse - from urllib.parse import unquote - from urllib.request import urlopen - basestring = unicode = str - iteritems = operator.methodcaller("items") -else: - from itertools import izip as zip - from urllib import unquote - from urllib2 import urlopen - import urlparse - iteritems = operator.methodcaller("iteritems") - - -FLOAT_TOLERANCE = 10 ** -15 -validators = {} - - -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 - return cls - return _validates - - -class UnknownType(Exception): - """ - An attempt was made to check if an instance was of an unknown type. - - """ - - -class RefResolutionError(Exception): - """ - A JSON reference failed to resolve. - - """ - - -class SchemaError(Exception): - """ - The provided schema is malformed. - - The same attributes are present as for :exc:`ValidationError`\s. - - """ - - def __init__(self, message, validator=None, path=()): - super(SchemaError, self).__init__(message, validator, path) - self.message = message - self.path = list(path) - self.validator = validator - - def __str__(self): - return self.message - - -class ValidationError(Exception): - """ - The instance didn't properly validate under the provided schema. - - Relevant attributes are: - * ``message`` : a human readable message explaining the error - * ``path`` : a list containing the path to the offending element (or [] - if the error happened globally) in *reverse* order (i.e. - deepest index first). - - """ - - def __init__(self, message, validator=None, path=()): - # Any validator that recurses (e.g. properties and items) must append - # to the ValidationError's path to properly maintain where in the - # instance the error occurred - super(ValidationError, self).__init__(message, validator, path) - self.message = message - self.path = list(path) - self.validator = validator - - def __str__(self): - return self.message - - -@validates("draft3") -class Draft3Validator(object): - """ - A validator for JSON Schema draft 3. - - """ - - DEFAULT_TYPES = { - "array": list, "boolean": bool, "integer": int, "null": type(None), - "number": (int, float), "object": dict, "string": basestring, - } - - def __init__(self, schema, types=(), resolver=None): - self._types = dict(self.DEFAULT_TYPES) - self._types.update(types) - - if resolver is None: - resolver = RefResolver.from_schema(schema) - - self.resolver = resolver - self.schema = schema - - def is_type(self, instance, type): - if type == "any": - return True - elif type not in self._types: - raise UnknownType(type) - type = self._types[type] - - # bool inherits from int, so ensure bools aren't reported as integers - if isinstance(instance, bool): - type = _flatten(type) - if int in type and bool not in type: - return False - return isinstance(instance, type) - - 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( - error.message, validator=error.validator, path=error.path, - ) - - def iter_errors(self, instance, _schema=None): - if _schema is None: - _schema = self.schema - - for k, v in iteritems(_schema): - validator = getattr(self, "validate_%s" % (k.lstrip("$"),), None) - - if validator is None: - continue - - errors = validator(v, instance, _schema) or () - for error in errors: - # set the validator if it wasn't already set by the called fn - if error.validator is None: - error.validator = k - yield error - - def validate(self, *args, **kwargs): - for error in self.iter_errors(*args, **kwargs): - raise error - - def validate_type(self, types, instance, schema): - types = _list(types) - - for type in types: - if self.is_type(type, "object"): - if self.is_valid(instance, type): - return - elif self.is_type(type, "string"): - if self.is_type(instance, type): - return - else: - 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.iter_errors(instance[property], subschema): - error.path.append(property) - yield error - elif subschema.get("required", False): - yield ValidationError( - "%r is a required property" % (property,), - validator="required", - path=[property], - ) - - 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.match(pattern, k): - for error in self.iter_errors(v, subschema): - 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.iter_errors(instance[extra], aP): - yield error - elif not aP and extras: - error = "Additional properties are not allowed (%s %s unexpected)" - yield ValidationError(error % _extras_msg(extras)) - - 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.iter_errors(instance, dependency): - 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_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.iter_errors(item, items): - error.path.append(index) - yield error - else: - for (index, item), subschema in zip(enumerate(instance), items): - for error in self.iter_errors(item, subschema): - error.path.append(index) - yield error - - def validate_additionalItems(self, aI, instance, schema): - if ( - not self.is_type(instance, "array") or - not self.is_type(schema.get("items"), "array") - ): - return - - if self.is_type(aI, "object"): - for item in instance[len(schema):]: - for error in self.iter_errors(item, aI): - 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_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.match(patrn, instance): - yield ValidationError("%r does not match %r" % (instance, patrn)) - - 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_enum(self, enums, instance, schema): - if instance not in enums: - yield ValidationError("%r is not one of %r" % (instance, enums)) - - def validate_divisibleBy(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 divisible by %r" % (instance, dB)) - - 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"): - extends = [extends] - for subschema in extends: - for error in self.iter_errors(instance, subschema): - yield error - - def validate_ref(self, ref, instance, schema): - resolved = self.resolver.resolve(ref) - for error in self.iter_errors(instance, resolved): - yield error - - -Draft3Validator.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" - }, -} - - -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 - - """ - - def __init__(self, base_uri, referrer, store=()): - self.base_uri = base_uri - self.referrer = referrer - self.store = dict(store, **_meta_schemas()) - - @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) - - def resolve(self, ref): - """ - Resolve a JSON ``ref``. - - :argument str ref: reference to resolve - :returns: the referrant document - - """ - - base_uri = self.base_uri - uri, fragment = urlparse.urldefrag(urlparse.urljoin(base_uri, ref)) - - if uri in self.store: - document = self.store[uri] - elif not uri or uri == self.base_uri: - document = self.referrer - else: - document = self.resolve_remote(uri) - - return self.resolve_fragment(document, fragment.lstrip("/")) - - 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 - - """ - - parts = unquote(fragment).split("/") if fragment else [] - - for part in parts: - part = part.replace("~1", "/").replace("~0", "~") - - if part not in document: - raise RefResolutionError( - "Unresolvable JSON pointer: %r" % fragment - ) - - document = document[part] - - return document - - def resolve_remote(self, uri): - """ - Resolve a remote ``uri``. - - Does not check the store first. - - :argument str uri: the URI to resolve - :returns: the retrieved document - - """ - - return json.load(urlopen(uri)) - - -class ErrorTree(object): - """ - ErrorTrees make it easier to check which validations failed. - - """ - - def __init__(self, errors=()): - self.errors = {} - self._contents = collections.defaultdict(self.__class__) - - for error in errors: - container = self - for element in reversed(error.path): - container = container[element] - container.errors[error.validator] = error - - def __contains__(self, k): - return k in self._contents - - def __getitem__(self, k): - """ - Retrieve the child tree with key ``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 _meta_schemas(): - """ - Collect the urls and meta schemas from each known validator. - - """ - - meta_schemas = (v.META_SCHEMA for v in validators.values()) - return dict((urlparse.urldefrag(m["id"])[0], m) for m in meta_schemas) - - -def _find_additional_properties(instance, schema): - """ - Return the set of additional properties for the given ``instance``. - - Weeds out properties that should have been validated by ``properties`` and - / or ``patternProperties``. - - Assumes ``instance`` is dict-like already. - - """ - - properties = schema.get("properties", {}) - patterns = "|".join(schema.get("patternProperties", {})) - for property in instance: - if property not in properties: - if patterns and re.search(patterns, property): - continue - yield property - - -def _extras_msg(extras): - """ - Create an error message for extra items or properties. - - """ - - if len(extras) == 1: - verb = "was" - else: - verb = "were" - return ", ".join(repr(extra) for extra in extras), verb - - -def _types_msg(instance, types): - """ - Create an error message for a failure to match the given types. - - If the ``instance`` is an object and contains a ``name`` property, it will - be considered to be a description of that object and used as its type. - - Otherwise the message is simply the reprs of the given ``types``. - - """ - - reprs = [] - for type in types: - try: - reprs.append(repr(type["name"])) - except Exception: - reprs.append(repr(type)) - return "%r is not of type %s" % (instance, ", ".join(reprs)) - - -def _flatten(suitable_for_isinstance): - """ - isinstance() can accept a bunch of really annoying different types: - * a single type - * a tuple of types - * an arbitrary nested tree of tuples - - Return a flattened tuple of the given argument. - - """ - - types = set() - - if not isinstance(suitable_for_isinstance, tuple): - suitable_for_isinstance = (suitable_for_isinstance,) - for thing in suitable_for_isinstance: - if isinstance(thing, tuple): - types.update(_flatten(thing)) - else: - types.add(thing) - return tuple(types) - - -def _list(thing): - """ - Wrap ``thing`` in a list if it's a single str. - - Otherwise, return it unchanged. - - """ - - if isinstance(thing, basestring): - return [thing] - return thing - - -def _delist(thing): - """ - Unwrap ``thing`` to a single element if its a single str in a list. - - Otherwise, return it unchanged. - - """ - - if ( - isinstance(thing, list) and - len(thing) == 1 - and isinstance(thing[0], basestring) - ): - return thing[0] - return thing - - -def _unbool(element, true=object(), false=object()): - """ - A hack to make True and 1 and False and 0 unique for _uniq. - - """ - - if element is True: - return true - elif element is False: - return false - return element - - -def _uniq(container): - """ - Check if all of a container's elements are unique. - - Successively tries first to rely that the elements are hashable, then - falls back on them being sortable, and finally falls back on brute - force. - - """ - - try: - return len(set(_unbool(i) for i in container)) == len(container) - except TypeError: - try: - sort = sorted(_unbool(i) for i in container) - sliced = itertools.islice(sort, 1, None) - for i, j in zip(sort, sliced): - if i == j: - return False - except (NotImplementedError, TypeError): - seen = [] - for e in container: - e = _unbool(e) - if e in seen: - return False - seen.append(e) - return True - - -def validate(instance, schema, cls=Draft3Validator, *args, **kwargs): - """ - Validate an ``instance`` under the given ``schema``. - - >>> validate([2, 3, 4], {"maxItems" : 2}) - Traceback (most recent call last): - ... - ValidationError: [2, 3, 4] is too long - - :func:`validate` will first verify that the provided schema is itself - valid, since not doing so can lead to less obvious error messages and fail - in less obvious or consistent ways. If you know you have a valid schema - already or don't care, you might prefer using the ``validate`` method - directly on a specific validator (e.g. :meth:`Draft3Validator.validate`). - - ``cls`` is a validator class that will be used to validate the instance. - By default this is a draft 3 validator. Any other provided positional and - keyword arguments will be provided to this class when constructing a - validator. - - :raises: - :exc:`ValidationError` if the instance is invalid - - :exc:`SchemaError` if the schema itself is invalid - - """ - - cls.check_schema(schema) - cls(schema, *args, **kwargs).validate(instance) |