Source code for kaiju_tools.jsonschema

"""Python classes for jsonschema validators."""

import abc
from collections.abc import Callable, Collection
from datetime import date, datetime
from uuid import UUID

import fastjsonschema
from fastjsonschema import compile
from fastjsonschema.exceptions import JsonSchemaException

from kaiju_tools.encoding import Serializable


__all__ = (
    'STRING_FORMATS',
    'compile_schema',
    'JSONSchemaObject',
    'Enumerated',
    'Boolean',
    'String',
    'Number',
    'Integer',
    'Array',
    'Object',
    'Generic',
    'JSONSchemaKeyword',
    'AnyOf',
    'OneOf',
    'AllOf',
    'Not',
    'GUID',
    'Date',
    'DateTime',
    'Null',
    'Constant',
)


# This 'hack' allows to validate datetime and uuid objects as if they were formatted strings.


class _CodeGenerator(fastjsonschema.CodeGeneratorDraft07):
    """Patch for the code generator to accept datetime and uuid objects."""

    def __init__(self, *args, **kws):
        super().__init__(*args, **kws)
        self._extra_imports_lines.extend(['from uuid import UUID', 'from datetime import datetime, date'])
        self._extra_imports_objects.update({'UUID': UUID, 'datetime': datetime, 'date': date})


def _get_code_generator_class(schema):
    return _CodeGenerator


fastjsonschema._get_code_generator_class = _get_code_generator_class
fastjsonschema.draft04.JSON_TYPE_TO_PYTHON_TYPE['string'] = 'str, datetime, date, UUID'

#


[docs]class JSONSchemaObject(Serializable): """Base JSONSchema object.""" type: str = None __slots__ = ('default', 'title', 'description', 'examples', 'enum', 'nullable')
[docs] def __init__( self, *, title: str = None, description: str = None, default=..., examples: list = None, enum: list = None, nullable: bool = None, ): """Initialize. :param title: short description :param description: long description :param default: default value :param examples: value examples :param enum: accepted list of values :param nullable: not used """ self.default = default self.title = title self.description = description self.examples = examples self.enum = enum self.nullable = nullable
def _set_non_null_values(self, data, keys) -> None: for key in keys: value = getattr(self, key) if value is not None: if isinstance(value, JSONSchemaObject): value = value.repr() data[key] = value def repr(self) -> dict: """Serialize.""" data = {} if self.type: data['type'] = self.type self._set_non_null_values(data, ('title', 'description', 'examples', 'enum')) if self.default is not ...: data['default'] = self.default return data
[docs]class Boolean(JSONSchemaObject): """Boolean `True` or `False`.""" type = 'boolean'
Enumerated = JSONSchemaObject # compatibility, the base object has enum # noinspection PyPep8Naming
[docs]class String(JSONSchemaObject): """Text/string data type.""" type = 'string' format_: str = None STRING_FORMATS = frozenset( { 'date-time', 'time', 'date', 'email', 'idn-email', 'hostname', 'idn-hostname', 'ipv4', 'ipv6', 'uri', 'uri-reference', 'iri', 'iri-reference', 'regex', } ) __slots__ = ('minLength', 'maxLength', 'pattern', 'format')
[docs] def __init__(self, *, minLength: int = None, maxLength: int = None, pattern: str = None, format: str = None, **kws): """Initialize. :param args: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :params kws: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :param minLength: min string size :param maxLength: max string size :param pattern: regex pattern :param format: string format (not working?) """ super().__init__(**kws) self.minLength = minLength self.maxLength = maxLength self.pattern = pattern if format and format not in self.STRING_FORMATS: raise JsonSchemaException( 'Invalid string format "%s".' 'Must be one of: "%s".' % (format, list(self.STRING_FORMATS)) ) self.format = self.format_ if self.format_ else format
def repr(self) -> dict: """Serialize.""" data = super().repr() self._set_non_null_values(data, ('minLength', 'maxLength', 'pattern', 'format')) return data
[docs]class DateTime(String): """Datetime string alias.""" format_ = 'date-time' __slots__ = tuple()
[docs]class Date(String): """Date string alias.""" format_ = 'date' __slots__ = tuple()
[docs]class GUID(String): """UUID string alias.""" format_ = 'uuid' __slots__ = tuple()
[docs]class Constant(Enumerated): """Value is a constant.""" __slots__ = tuple()
[docs] def __init__(self, const, **kws): """Initialize. :param args: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :params kws: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :param const: constant value """ super().__init__(enum=[const], **kws)
[docs]class Null(Enumerated): """Null value only.""" __slots__ = tuple() def __init__(self): """Initialize.""" super().__init__(enum=[None])
# noinspection PyPep8Naming
[docs]class Number(JSONSchemaObject): """Numeric data type (use it for both float or integer params).""" type = 'number' __slots__ = ('multipleOf', 'minimum', 'exclusiveMinimum', 'maximum', 'exclusiveMaximum')
[docs] def __init__( self, *, multipleOf: float = None, minimum: float = None, maximum: float = None, exclusiveMinimum: float = None, exclusiveMaximum: float = None, **kws, ): """Initialize. :param args: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :params kws: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :param multipleOf: value should be a multiplier of :param minimum: min allowed value :param maximum: max allowed value :param exclusiveMinimum: min allowed value (excl) :param exclusiveMaximum: max allowed value (excl) """ super().__init__(**kws) self.multipleOf = multipleOf self.minimum = minimum self.maximum = maximum self.exclusiveMinimum = exclusiveMinimum self.exclusiveMaximum = exclusiveMaximum
def repr(self) -> dict: """Serialize.""" data = super().repr() self._set_non_null_values(data, ('multipleOf', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum')) return data
[docs]class Integer(Number): """Integer type.""" type = 'integer' __slots__ = tuple()
# noinspection PyPep8Naming
[docs]class Array(JSONSchemaObject): """Array, list, set or tuple definition (depends on params).""" type = 'array' __slots__ = ('items', 'prefixItems', 'contains', 'additionalItems', 'uniqueItems', 'minItems', 'maxItems')
[docs] def __init__( self, items: JSONSchemaObject = None, *, prefixItems: Collection[JSONSchemaObject] = None, contains: JSONSchemaObject = None, additionalItems: bool = None, uniqueItems: bool = None, minItems: int = None, maxItems: int = None, **kws, ): """Initialize. :params kws: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :param items: item type :param prefixItems: use this to create a tuple like structure with different types of items :param contains: a list should contain this type of objects :param additionalItems: allow additional items in the tuple :param uniqueItems: unique items - set :param minItems: min number of items :param maxItems: max number of items """ super().__init__(**kws) self.items = items self.prefixItems = prefixItems self.contains = contains self.additionalItems = additionalItems self.uniqueItems = uniqueItems self.minItems = minItems self.maxItems = maxItems
@staticmethod def _unpack_items(data: dict, key: str): if key in data: data[key] = [item.repr() for item in data[key]] def repr(self) -> dict: data = super().repr() self._set_non_null_values( data, ('items', 'prefixItems', 'contains', 'additionalItems', 'uniqueItems', 'minItems', 'maxItems') ) for key in ('prefixItems',): self._unpack_items(data, key) return data
# noinspection PyPep8Naming
[docs]class Object(JSONSchemaObject): """JSON object (dictionary) definition.""" type = 'object' __slots__ = ( 'properties', 'propertyNames', 'required', 'patternProperties', 'additionalProperties', 'minProperties', 'maxProperties', )
[docs] def __init__( self, properties: dict[str, JSONSchemaObject] = None, *, patternProperties: dict[str, JSONSchemaObject] = None, propertyNames: dict = None, additionalProperties: bool = None, minProperties: int = None, maxProperties: int = None, required: list[str] = None, **kws, ): """Initialize. :params kws: see :py:class:`~kaiju_tools.jsonschema.JSONSchemaObject` :param properties: object attributes schema :param patternProperties: object attribute patterns schema :param propertyNames: allowed property names :param additionalProperties: allow additional attributes :param minProperties: min number of properties :param maxProperties: max number of properties :param required: list of required keys """ super().__init__(**kws) self.properties = properties if properties else {} self.patternProperties = patternProperties self.propertyNames = propertyNames self.additionalProperties = additionalProperties self.minProperties = minProperties self.maxProperties = maxProperties self.required = required
def repr(self) -> dict: """Serialize.""" data = super().repr() self._set_non_null_values( data, ( 'properties', 'propertyNames', 'required', 'patternProperties', 'additionalProperties', 'minProperties', 'maxProperties', ), ) data['properties'] = {key: value.repr() for key, value in data['properties'].items()} if 'patternProperties' in data: data['patternProperties'] = {key: value.repr() for key, value in data['patternProperties'].items()} return data
Generic = JSONSchemaObject # compatibility class JSONSchemaKeyword(JSONSchemaObject, abc.ABC): """Abstract class for JSON Schema specific logical keywords.""" type: str = None __slots__ = ('items',) def __init__(self, *items: JSONSchemaObject): """Initialize.""" super().__init__() self.items = items def repr(self) -> dict: """Serialize.""" return {self.type: [item.repr() for item in self.items]}
[docs]class AnyOf(JSONSchemaKeyword): """Given data must be valid against any (one or more) of the given sub-schemas.""" type = 'anyOf' __slots__ = tuple()
[docs]class OneOf(JSONSchemaKeyword): """Given data must be valid against exactly one of the given sub-schemas.""" type = 'oneOf' __slots__ = tuple()
[docs]class AllOf(JSONSchemaKeyword): """Given data must be valid against all of the given sub-schemas.""" type = 'allOf' __slots__ = tuple()
class Nullable(AnyOf): """Nullable value.""" type = 'oneOf' __slots__ = tuple() def __init__(self, item: JSONSchemaObject, /): """Initialize.""" super().__init__(item, Null())
[docs]class Not(JSONSchemaObject): """Reverse the condition.""" __slots__ = ('item',)
[docs] def __init__(self, item: JSONSchemaObject, /): """Initialize. :param item: schema to create negative condition from """ super().__init__() self.item = item
def repr(self): """Serialize.""" return {'not': self.item.repr()}
STRING_FORMATS = { 'uuid': r'^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$' } # these are used by the fastjsonschema compiler to validate some specific data types
[docs]def compile_schema(validator: JSONSchemaObject | dict, /) -> Callable: """Compile JSONSchema object into a validator function.""" if isinstance(validator, JSONSchemaObject): validator = validator.repr() return compile(validator, formats=STRING_FORMATS)