Source code for kwcoco.util.jsonschema_elements

r"""
Functional interface into defining jsonschema structures.

See mixin classes for details.

Example:
    >>> from kwcoco.util.jsonschema_elements import *  # NOQA
    >>> elem = SchemaElements()
    >>> for base in SchemaElements.__bases__:
    >>>     print('\n\n====\nbase = {!r}'.format(base))
    >>>     attrs = [key for key in dir(base) if not key.startswith('_')]
    >>>     for key in attrs:
    >>>         value = getattr(elem, key)
    >>>         print('{} = {}'.format(key, value))

"""
import ubelt as ub


[docs]class Element(dict): """ A dictionary used to define an element of a JSON Schema. The exact keys/values for the element will depend on the type of element being described. The :class:`SchemaElements` defines exactly what these are for the core elements. (e.g. OBJECT, INTEGER, NULL, ARRAY, ANYOF) Example: >>> from kwcoco.coco_schema import * # NOQA >>> self = Element(base={'type': 'demo'}, options={'opt1', 'opt2'}) >>> new = self(opt1=3) >>> print('self = {}'.format(ub.repr2(self, nl=1, sort=1))) >>> print('new = {}'.format(ub.repr2(new, nl=1, sort=1))) >>> print('new2 = {}'.format(ub.repr2(new(), nl=1, sort=1))) >>> print('new3 = {}'.format(ub.repr2(new(title='myvar'), nl=1, sort=1))) >>> print('new4 = {}'.format(ub.repr2(new(title='myvar')(examples=['']), nl=1, sort=1))) >>> print('new5 = {}'.format(ub.repr2(new(badattr=True), nl=1, sort=1))) self = { 'type': 'demo', } new = { 'opt1': 3, 'type': 'demo', } new2 = { 'opt1': 3, 'type': 'demo', } new3 = { 'opt1': 3, 'title': 'myvar', 'type': 'demo', } new4 = { 'examples': [''], 'opt1': 3, 'title': 'myvar', 'type': 'demo', } new5 = { 'opt1': 3, 'type': 'demo', } """ # These are generic attributes that any type of schema element might have
[docs] __generics__ = { 'title': NotImplemented, 'description': NotImplemented, 'default': NotImplemented, 'examples': NotImplemented
} def __init__(self, base, options={}, _magic=None): """ Args: base (dict): the keys / values this schema must contain options (options): the keys / values this schema may contain _magic (callable): called when creating an instance of this schema element. Allows convinience attributes to be converted to the formal jsonschema specs. TODO: _magic is a terrible name, we need to rename it with something descriptive. """ self._base = base if isinstance(options, (set, list, tuple)): options = {k: None for k in options} self._options = ub.dict_union(options, self.__generics__) self._magic = _magic super().__init__(base)
[docs] def __call__(self, *args, **kw): # newbase = base | (kw & options) # imagine all the syntax if self._magic: kw = self._magic(kw) newbase = ub.dict_union(self._base, ub.dict_isect(kw, self._options)) new = Element(newbase, self._options, self._magic) return new
[docs] def validate(self, instance=ub.NoParam): """ If ``instance`` is given, validates that that dictionary conforms to this schema. Otherwise validates that this is a valid schema element. Args: instance (dict): a dictionary to validate """ import jsonschema from jsonschema.validators import validator_for if instance is ub.NoParam: cls = validator_for(self) cls.check_schema(self) return self else: return jsonschema.validate(instance, schema=self)
[docs] def __or__(self, other): """ Syntax for making an anyOf relationship Example: >>> from kwcoco.util.jsonschema_elements import * # NOQA >>> obj1 = OBJECT(dict(opt1=NUMBER())) >>> obj2 = OBJECT(dict(opt2=STRING())) >>> obj3 = OBJECT(dict(opt3=ANY())) >>> any_v1 = obj1 | obj2 >>> any_v2 = ANYOF(obj1, obj2) >>> assert any_v1 == any_v2 >>> any_v3 = any_v1 | obj3 >>> any_v4 = ANYOF(obj1, obj2, obj3) >>> assert any_v3 == any_v4 """ unpacked = [] for item in [self, other]: if list(item.keys()) == ['anyOf']: unpacked.extend(item['anyOf']) else: unpacked.append(item) return ANYOF(*unpacked)
[docs]class ScalarElements(object): """ Single-valued elements """ @property
[docs] def NULL(self): """ https://json-schema.org/understanding-json-schema/reference/null.html """ return Element(base={'type': 'null'}, options={})
@property
[docs] def BOOLEAN(self): """ https://json-schema.org/understanding-json-schema/reference/null.html """ return Element(base={'type': 'boolean'}, options={})
@property
[docs] def STRING(self): """ https://json-schema.org/understanding-json-schema/reference/string.html """ return Element(base={'type': 'string'}, options={ 'pattern': 'some regex', 'enum': ['Street', 'Avenue', 'Boulevard'],
}) @property
[docs] def NUMBER(self): """ https://json-schema.org/understanding-json-schema/reference/numeric.html#number """ return Element(base={'type': 'number'}, options={ 'minimum': 0, 'maximum': 100, 'exclusiveMaximum': True,
}) @property
[docs] def INTEGER(self): """ https://json-schema.org/understanding-json-schema/reference/numeric.html#integer """ return Element(base={'type': 'integer'}, options={ 'minimum': 0, 'maximum': 100, 'exclusiveMaximum': True,
})
[docs]class QuantifierElements(object): """ Quantifier types https://json-schema.org/understanding-json-schema/reference/combining.html#allof Example: >>> from kwcoco.util.jsonschema_elements import * # NOQA >>> elem.ANYOF(elem.STRING, elem.NUMBER).validate() >>> elem.ONEOF(elem.STRING, elem.NUMBER).validate() >>> elem.NOT(elem.NULL).validate() >>> elem.NOT(elem.ANY).validate() >>> elem.ANY.validate() """ @property
[docs] def ANY(self): return Element({})
[docs] def ALLOF(self, *TYPES): return Element({'allOf': list(TYPES)})
[docs] def ANYOF(self, *TYPES): return Element({'anyOf': list(TYPES)})
[docs] def ONEOF(self, *TYPES): return Element({'oneOf': list(TYPES)})
[docs] def NOT(self, TYPE): return Element({'not': TYPE})
[docs]class ContainerElements: """ Types that contain other types Example: >>> from kwcoco.util.jsonschema_elements import * # NOQA >>> print(elem.ARRAY().validate()) >>> print(elem.OBJECT().validate()) >>> print(elem.OBJECT().validate()) {'type': 'array', 'items': {}} {'type': 'object', 'properties': {}} {'type': 'object', 'properties': {}} """
[docs] def ARRAY(self, TYPE={}, **kw): """ https://json-schema.org/understanding-json-schema/reference/array.html Example: >>> from kwcoco.util.jsonschema_elements import * # NOQA >>> ARRAY(numItems=3) >>> schema = ARRAY(minItems=3) >>> schema.validate() {'type': 'array', 'items': {}, 'minItems': 3} """ def _magic(kw): numItems = kw.pop('numItems', None) if numItems is not None: kw.update({ 'minItems': numItems, 'maxItems': numItems, }) return kw self = Element( base={'type': 'array', 'items': TYPE}, options={ 'contains': {'type': 'number'}, 'minItems': 2, 'maxItems': 3, 'uniqueItems': True, 'additionalItems': {'type': 'string'} }, _magic=_magic) return self(**_magic(kw))
[docs] def OBJECT(self, PROPERTIES={}, **kw): """ https://json-schema.org/understanding-json-schema/reference/object.html Example: >>> import jsonschema >>> schema = elem.OBJECT() >>> jsonschema.validate({}, schema) >>> # >>> import jsonschema >>> schema = elem.OBJECT({ >>> 'key1': elem.ANY(), >>> 'key2': elem.ANY(), >>> }, required=['key1']) >>> jsonschema.validate({'key1': None}, schema) >>> # >>> import jsonschema >>> schema = elem.OBJECT({ >>> 'key1': elem.OBJECT({'arr': elem.ARRAY()}), >>> 'key2': elem.ANY(), >>> }, required=['key1'], title='a title') >>> schema.validate() >>> print('schema = {}'.format(ub.repr2(schema, sort=1, nl=-1))) >>> jsonschema.validate({'key1': {'arr': []}}, schema) schema = { 'properties': { 'key1': { 'properties': { 'arr': {'items': {}, 'type': 'array'} }, 'type': 'object' }, 'key2': {} }, 'required': ['key1'], 'title': 'a title', 'type': 'object' } """ self = Element( base={'type': 'object', 'properties': PROPERTIES}, options={ 'additionalProperties': False, 'required': [], 'propertyNames': [], 'minProperties': 0, 'maxProperties': float('inf'), 'dependencies': {}, 'oneOf': {}, # hack to allow for multiple required 'anyOf': {}, # hack to allow for multiple required } ) return self(**kw)
[docs]class SchemaElements( ScalarElements, QuantifierElements, ContainerElements, ): """ Functional interface into defining jsonschema structures. See mixin classes for details. References: https://json-schema.org/understanding-json-schema/ TODO: - [ ] Generics: title, description, default, examples CommandLine: xdoctest -m /home/joncrall/code/kwcoco/kwcoco/util/jsonschema_elements.py SchemaElements Example: >>> from kwcoco.util.jsonschema_elements import * # NOQA >>> elem = SchemaElements() >>> elem.ARRAY(elem.ANY()) >>> schema = OBJECT({ >>> 'prop1': ARRAY(INTEGER, minItems=3), >>> 'prop2': ARRAY(STRING, numItems=2), >>> 'prop3': ARRAY(OBJECT({ >>> 'subprob1': NUMBER, >>> 'subprob2': NUMBER, >>> })) >>> }) >>> print('schema = {}'.format(ub.repr2(schema, nl=2, sort=1))) schema = { 'properties': { 'prop1': {'items': {'type': 'integer'}, 'minItems': 3, 'type': 'array'}, 'prop2': {'items': {'type': 'string'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'prop3': {'items': {'properties': {'subprob1': {'type': 'number'}, 'subprob2': {'type': 'number'}}, 'type': 'object'}, 'type': 'array'}, }, 'type': 'object', } >>> TYPE = elem.OBJECT({ >>> 'p1': ANY, >>> 'p2': ANY, >>> }, required=['p1']) >>> import jsonschema >>> inst = {'p1': None} >>> jsonschema.validate(inst, schema=TYPE) >>> #jsonschema.validate({'p2': None}, schema=TYPE) """
if 0: """ from kwcoco.util.jsonschema_elements import * # NOQA elem = SchemaElements() attrs = [key for key in dir(SchemaElements) if not key.startswith('_')] print('elem = SchemaElements()') for key in attrs: print('{key} = elem.{key}'.format(key=key)) """
[docs]elem = SchemaElements()
[docs]ALLOF = elem.ALLOF
[docs]ANY = elem.ANY
[docs]ANYOF = elem.ANYOF
[docs]ARRAY = elem.ARRAY
[docs]BOOLEAN = elem.BOOLEAN
[docs]INTEGER = elem.INTEGER
[docs]NOT = elem.NOT
[docs]NULL = elem.NULL
[docs]NUMBER = elem.NUMBER
[docs]OBJECT = elem.OBJECT
[docs]ONEOF = elem.ONEOF
[docs]STRING = elem.STRING
if __name__ == '__main__': """ CommandLine: python ~/code/kwcoco/kwcoco/util/jsonschema_elements.py all """ import xdoctest xdoctest.doctest_module(__file__)