mirror of
https://github.com/clearml/clearml-server
synced 2025-01-31 10:56:48 +00:00
189 lines
5.9 KiB
Python
189 lines
5.9 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from collections import OrderedDict
|
|
|
|
import attr
|
|
import related
|
|
import six
|
|
from related import StringField, FloatField, IntegerField, BooleanField
|
|
|
|
|
|
@attr.s
|
|
class AtomicType(object):
|
|
python = attr.ib()
|
|
related = attr.ib()
|
|
|
|
|
|
ATOMIC_TYPES = {
|
|
'integer': AtomicType(int, IntegerField),
|
|
'number': AtomicType(float, FloatField),
|
|
'string': AtomicType(str, StringField),
|
|
'boolean': AtomicType(bool, BooleanField),
|
|
}
|
|
|
|
|
|
def resolve_ref(definitions, value):
|
|
ref = value.get('$ref')
|
|
if ref:
|
|
name = ref.split('/')[-1]
|
|
return definitions[name]
|
|
one_of = value.get('oneOf')
|
|
if one_of:
|
|
one_of = list(one_of)
|
|
one_of.remove(dict(type='null'))
|
|
assert len(one_of) == 1
|
|
return dict(value, **resolve_ref(definitions, one_of.pop()))
|
|
return value
|
|
|
|
|
|
def normalize_type(typ):
|
|
if isinstance(typ, six.string_types):
|
|
return typ
|
|
return (set(typ) - {'null'}).pop()
|
|
|
|
|
|
@attr.s
|
|
class RelatedBuilder(object):
|
|
"""
|
|
Converts jsonschema to related class or field.
|
|
:param name: Object name. Will be used as the name of the class and to detect recursive objects,
|
|
which are not supported.
|
|
:param schema: jsonschema which is the base of the object
|
|
:param required: In case of child fields, whether the field is required. Only used in ``to_field``.
|
|
:param definitions: Dictionary to resolve definitions. Defaults to schema['definitions'].
|
|
:param default: Default value of field. Only used in ``to_field``.
|
|
"""
|
|
|
|
name = attr.ib(type=six.text_type)
|
|
schema = attr.ib(type=dict, repr=False)
|
|
required = attr.ib(type=bool, default=False)
|
|
definitions = attr.ib(type=dict, default=None, repr=False)
|
|
default = attr.ib(default=attr.Factory(lambda: attr.NOTHING))
|
|
|
|
def __attrs_post_init__(self):
|
|
self.schema = resolve_ref(self.definitions, self.schema)
|
|
self.type = normalize_type(self.schema['type'])
|
|
self.definitions = self.definitions or self.schema.get('definitions')
|
|
|
|
def to_field(self):
|
|
"""
|
|
Creates the appropriate ``related`` field from instance.
|
|
NOTE: Items and nesting level of nested arrays will not be checked.
|
|
"""
|
|
if self.type in ATOMIC_TYPES:
|
|
field = ATOMIC_TYPES[self.type].related
|
|
return field(default=self.default, required=self.required)
|
|
if self.type == 'array':
|
|
sub_schema = self.schema['items']
|
|
builder = RelatedBuilder(
|
|
'{}_items'.format(self.name), sub_schema, definitions=self.definitions
|
|
)
|
|
return related.SequenceField(
|
|
list if builder.type == 'array' else builder.to_class(),
|
|
default=attr.Factory(list),
|
|
)
|
|
if self.schema.get('additionalProperties') is True:
|
|
return attr.ib(type=dict, default=None)
|
|
|
|
return related.ChildField(
|
|
self.to_class(), default=self.default, required=self.required
|
|
)
|
|
|
|
def to_class(self):
|
|
"""
|
|
Creates a related class.
|
|
"""
|
|
required = self.schema.get('required', [])
|
|
|
|
if self.type in ATOMIC_TYPES:
|
|
return ATOMIC_TYPES[self.type].python
|
|
|
|
if self.type == 'array':
|
|
raise RuntimeError(self, 'Cannot convert array to related class')
|
|
|
|
assert self.type and normalize_type(self.type) == 'object', (
|
|
self.type,
|
|
list(self.schema),
|
|
)
|
|
properties = sorted(
|
|
tuple(
|
|
(
|
|
inner_name,
|
|
RelatedBuilder(
|
|
name=inner_name,
|
|
schema=value,
|
|
required=inner_name in required,
|
|
definitions=definitions,
|
|
),
|
|
)
|
|
for inner_name, value in self.schema['properties'].items()
|
|
if inner_name != self.name
|
|
),
|
|
key=lambda pair: pair[1].required,
|
|
reverse=True,
|
|
)
|
|
return related.mutable(
|
|
type(
|
|
self.name,
|
|
(object,),
|
|
OrderedDict([(key, builder.to_field()) for key, builder in properties]),
|
|
)
|
|
)
|
|
|
|
|
|
class Visitor(object):
|
|
"""Base class for visitors."""
|
|
|
|
def visit(self, node, *args, **kwargs):
|
|
"""Visit a node.
|
|
|
|
Calls ``visit_CLASSNAME`` on itself passing ``node``, where
|
|
``CLASSNAME`` is the node's class. If the visitor does not implement an
|
|
appropriate visitation method, will go up the
|
|
`MRO <https://www.python.org/download/releases/2.3/mro/>`_ until a
|
|
match is found.
|
|
|
|
If the search exhausts all classes of node, raises a
|
|
:class:`~exceptions.NotImplementedError`.
|
|
|
|
:param node: The node to visit.
|
|
:return: The return value of the called visitation function.
|
|
"""
|
|
if isinstance(node, type):
|
|
mro = node.mro()
|
|
else:
|
|
mro = type(node).mro()
|
|
for cls in mro:
|
|
meth = getattr(self, 'visit_' + cls.__name__, None)
|
|
if meth is None:
|
|
continue
|
|
return meth(node, *args, **kwargs)
|
|
|
|
raise NotImplementedError(
|
|
'No visitation method visit_{}'.format(node.__class__.__name__)
|
|
)
|
|
|
|
|
|
class SchemaCleaner(Visitor):
|
|
def __init__(self, definitions):
|
|
self.definitions = definitions
|
|
|
|
def visit_dict(self, obj, schema):
|
|
schema = resolve_ref(self.definitions, schema)
|
|
if schema.get('additionalProperties') is True:
|
|
return
|
|
props = schema['properties']
|
|
for key, value in list(obj.items()):
|
|
if key in props:
|
|
self.visit(value, props[key])
|
|
else:
|
|
del obj[key]
|
|
|
|
def visit_list(self, obj, schema):
|
|
for value in obj:
|
|
self.visit(value, schema['items'])
|
|
|
|
@staticmethod
|
|
def visit_object(obj, schema):
|
|
pass
|