Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 47 additions & 31 deletions src/rune/runtime/base_data_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):

def __setattr__(self, name: str, value: Any) -> None:
if isinstance(value, Reference):
self.bind_property_to(name, value)
self._bind_property_to(name, value)
else:
# replace reference with an object
if name in self.get_rune_refs_container():
self.remove_rune_ref(name)
if name in self._get_rune_refs_container():
self._remove_rune_ref(name)
if isinstance(self.__dict__[name], _EnumWrapper):
self.__dict__[name] = _EnumWrapper()
# if the value is an enum, pass it to the EnumWrapper
Expand All @@ -45,14 +45,14 @@ def __setattr__(self, name: str, value: Any) -> None:
value = _EnumWrapper(value)
# if the value is a "model", register as rune_parent
if isinstance(value, BaseMetaDataMixin):
value.set_rune_parent(self)
value._set_rune_parent(self)
super().__setattr__(name, value)

@model_serializer(mode='wrap')
def _serialize_refs(self, serializer, info):
'''should replace objects with refs while serializing'''
res = serializer(self, info)
refs = self.get_rune_refs_container()
refs = self._get_rune_refs_container()
for property_nm, (key, ref_type) in refs.items():
res[property_nm] = {ref_type.rune_ref_tag: key}
res = self.__dict__.get(ROOT_CONTAINER, {}) | res
Expand All @@ -64,10 +64,21 @@ def _deserialize_refs(cls, data: Any,
handler: ModelWrapValidatorHandler[Self]) -> Self:
'''should resolve refs after creation'''
obj = handler(data)
obj.init_rune_parent()
obj.resolve_references()
obj._init_rune_parent() # pylint: disable=protected-access
obj.resolve_references(ignore_dangling=True, recurse=False)
return obj

def _init_rune_parent(self):
'''sets the rune parent in all properties'''
refs = self._get_rune_refs_container()
if not self.get_rune_parent() and RUNE_OBJ_MAPS not in self.__dict__:
self.__dict__[RUNE_OBJ_MAPS] = {}

for prop_nm, obj in self.__dict__.items():
if (isinstance(obj, BaseMetaDataMixin)
and not prop_nm.startswith('__') and prop_nm not in refs):
obj._set_rune_parent(self) # pylint: disable=protected-access

def rune_serialize(
self,
*,
Expand Down Expand Up @@ -146,8 +157,7 @@ def rune_serialize(

root_meta = self.__dict__.setdefault(ROOT_CONTAINER, {})
root_meta['@type'] = self._FQRTN
root_meta['@model'] = self._FQRTN.split(
'.', maxsplit=1)[0]
root_meta['@model'] = self._FQRTN.split('.', maxsplit=1)[0]
root_meta['@version'] = self.get_model_version()

return self.model_dump_json(indent=indent,
Expand All @@ -164,7 +174,7 @@ def rune_serialize(

@classmethod
def rune_deserialize(cls,
rune_json: str,
rune_data: str | dict[str, Any],
validate_model: bool = True,
check_rune_constraints: bool = True,
strict: bool = True,
Expand Down Expand Up @@ -192,37 +202,42 @@ def rune_deserialize(cls,
#### Returns:
`BaseModel:` The Rune model.
'''
rune_dict = json.loads(rune_json)
rune_dict.pop('@version', None)
rune_dict.pop('@model', None)
rune_cls = cls._type_to_cls(rune_dict)
model = rune_cls.model_validate(rune_dict, strict=strict)
if isinstance(rune_data, str):
rune_data = json.loads(rune_data)
elif not isinstance(rune_data, dict):
raise ValueError(f'rune_data is of type {type(rune_data)}, '
'alas it has to be either dict or str!')
rune_data.pop('@version', None)
rune_data.pop('@model', None)
rune_cls = cls._type_to_cls(rune_data)
model = rune_cls.model_validate(rune_data, strict=strict)
model.resolve_references(ignore_dangling=False, recurse=True)
if validate_model:
model.validate_model(check_rune_constraints=check_rune_constraints,
strict=strict,
raise_exc=raise_validation_errors)
return model

def init_rune_parent(self):
'''sets the rune parent in all properties'''
refs = self.get_rune_refs_container()
if not self.get_rune_parent() and RUNE_OBJ_MAPS not in self.__dict__:
self.__dict__[RUNE_OBJ_MAPS] = {}

for prop_nm, obj in self.__dict__.items():
if (isinstance(obj, BaseMetaDataMixin)
and not prop_nm.startswith('__') and prop_nm not in refs):
obj.set_rune_parent(self)

def resolve_references(self):
def resolve_references(self, ignore_dangling=False, recurse=True):
'''resolves all attributes which are references'''
if recurse:
for prop_nm, obj in self.__dict__.items():
if (isinstance(obj, BaseDataClass)
and not prop_nm.startswith('__')):
obj.resolve_references(ignore_dangling=ignore_dangling,
recurse=recurse)

refs = []
for prop_nm, obj in self.__dict__.items():
if isinstance(obj, (UnresolvedReference, Reference)):
refs.append((prop_nm, obj.get_reference(self)))
try:
refs.append((prop_nm, obj.get_reference(self)))
except KeyError:
if not ignore_dangling:
raise

for prop_nm, ref in refs:
self.bind_property_to(prop_nm, ref)
self._bind_property_to(prop_nm, ref)

def validate_model(self,
check_rune_constraints=True,
Expand All @@ -241,8 +256,9 @@ def validate_model(self,
att_errors = self.validate_attribs(raise_exc=raise_exc,
strict=strict)
if check_rune_constraints:
att_errors.extend(self.validate_conditions(
recursively=recursively, raise_exc=raise_exc))
att_errors.extend(
self.validate_conditions(recursively=recursively,
raise_exc=raise_exc))
return att_errors
finally:
self.enable_meta_checks()
Expand Down
136 changes: 63 additions & 73 deletions src/rune/runtime/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pydantic_core import PydanticCustomError
# from rune.runtime.object_registry import get_object

DEFAULT_META = '_ALLOWED_METADATA'
META_CONTAINER = '__rune_metadata'
REFS_CONTAINER = '__rune_references'
PARENT_PROP = '__rune_parent'
Expand Down Expand Up @@ -120,7 +121,7 @@ def get_reference(self, parent):

class BaseMetaDataMixin:
'''Base class for the meta data support of basic amd complex types'''
_DEFAULT_SCOPE_TYPE = 'cdm.event.common.TradeState.TradeState'
_DEFAULT_SCOPE_TYPE = 'cdm.event.common.TradeState'
__meta_check_disabled = False

@classmethod
Expand All @@ -138,29 +139,13 @@ def meta_checks_enabled(cls):
'''is metadata checked during deserialize'''
return not BaseMetaDataMixin.__meta_check_disabled

def _get_meta_container(self) -> dict[str, Any]:
return self.__dict__.get(META_CONTAINER, {})

def _check_props_allowed(self, props: dict[str, Any]):
if not props:
return
allowed = set(self._get_meta_container().keys())
prop_keys = set(props.keys())
if not prop_keys.issubset(allowed):
raise ValueError('Not allowed metadata provided: '
f'{prop_keys - allowed}')

def init_meta(self, allowed_meta: set[str]):
''' if not initialised, just creates empty meta slots. If the metadata
container is not empty, it will check if the already present keys
are conform to the allowed keys.
'''
meta = self.__dict__.setdefault(META_CONTAINER, {})
current_meta = set(meta.keys())
if not current_meta.issubset(allowed_meta):
raise ValueError(f'Allowed meta {allowed_meta} differs from the '
f'currently existing meta slots: {current_meta}')
meta |= {k: None for k in allowed_meta - current_meta}
def is_scope_instance(self):
'''is this object a scope for `scoped` keys/references'''
if not (scope := self._get_rune_scope_type()):
scope = self._DEFAULT_SCOPE_TYPE
if not (fqcn := getattr(self, '_FQRTN', None)):
fqcn = f'{self.__class__.__module__}.{self.__class__.__qualname__}'
return fqcn == scope

def set_meta(self, check_allowed=True, **kwds):
'''set some/all metadata properties'''
Expand Down Expand Up @@ -211,7 +196,40 @@ def get_object_by_key(self, key: str, key_type: KeyType):
'''retrieve an object with a key an key type'''
return self._get_object_map(key_type)[key]

def bind_property_to(self, property_nm: str, ref: Reference):
def get_rune_parent(self) -> Self | None:
'''the parent object'''
return self.__dict__.get(PARENT_PROP)

def _get_meta_container(self) -> dict[str, Any]:
return self.__dict__.get(META_CONTAINER, {})

def _merged_allowed_meta(self, allowed_meta: set[str]) -> set[str]:
default_meta = getattr(self, DEFAULT_META, set())
return set(allowed_meta) | default_meta

def _check_props_allowed(self, props: dict[str, Any]):
if not props:
return
allowed = self._merged_allowed_meta(self._get_meta_container().keys())
prop_keys = set(props.keys())
if not prop_keys.issubset(allowed):
raise ValueError('Not allowed metadata provided: '
f'{prop_keys - allowed}')

def _init_meta(self, allowed_meta: set[str]):
''' if not initialised, just creates empty meta slots. If the metadata
container is not empty, it will check if the already present keys
are conform to the allowed keys.
'''
allowed_meta = self._merged_allowed_meta(allowed_meta)
meta = self.__dict__.setdefault(META_CONTAINER, {})
current_meta = set(meta.keys())
if not current_meta.issubset(allowed_meta):
raise ValueError(f'Allowed meta {allowed_meta} differs from the '
f'currently existing meta slots: {current_meta}')
meta |= {k: None for k in allowed_meta - current_meta}

def _bind_property_to(self, property_nm: str, ref: Reference):
'''set the property to reference the object referenced by the key'''
allowed_ref_types = getattr(self, '_KEY_REF_CONSTRAINTS', {})
if ref.key_type.rune_ref_tag not in allowed_ref_types.get(
Expand Down Expand Up @@ -257,23 +275,7 @@ def _get_object_map(self, key_type: KeyType) -> dict[str, Any]:
# pylint: disable=protected-access
return self.get_rune_parent()._get_object_map(key_type) # type:ignore

@classmethod
def _create_unresolved_ref(cls, metadata) -> UnresolvedReference | None:
if ref := {k: v for k, v in metadata.items() if k.startswith('@ref')}:
if len(ref) != 1:
ref.pop(KeyType.INTERNAL.rune_ref_tag, None)
if len(ref) != 1:
ref.pop(KeyType.EXTERNAL.rune_ref_tag, None)
if len(ref) != 1:
raise ValueError(f'Multiple references found: {ref}!')
return UnresolvedReference(ref)
return None

def get_rune_parent(self) -> Self | None:
'''the parent object'''
return self.__dict__.get(PARENT_PROP)

def set_rune_parent(self, parent: Self):
def _set_rune_parent(self, parent: Self):
'''sets the parent object'''
self.__dict__[PARENT_PROP] = parent
if obj_maps := self.__dict__.pop(RUNE_OBJ_MAPS, None):
Expand Down Expand Up @@ -305,14 +307,26 @@ def _update_object_maps(self, new_maps):
f'Duplicated keys {dup_keys}')
local_map |= new_map

def get_rune_refs_container(self):
def _get_rune_refs_container(self):
'''return the dictionary of the refs held'''
return self.__dict__.get(REFS_CONTAINER, {})

def remove_rune_ref(self, name):
def _remove_rune_ref(self, name):
'''remove a reference'''
return self.__dict__[REFS_CONTAINER].pop(name)

@classmethod
def _create_unresolved_ref(cls, metadata) -> UnresolvedReference | None:
if ref := {k: v for k, v in metadata.items() if k.startswith('@ref')}:
if len(ref) != 1:
ref.pop(KeyType.INTERNAL.rune_ref_tag, None)
if len(ref) != 1:
ref.pop(KeyType.EXTERNAL.rune_ref_tag, None)
if len(ref) != 1:
raise ValueError(f'Multiple references found: {ref}!')
return UnresolvedReference(ref)
return None

@classmethod
@lru_cache
def _get_rune_scope_type(cls):
Expand All @@ -322,18 +336,11 @@ def _get_rune_scope_type(cls):
try:
module = importlib.import_module(
cls.__module__.split('.', maxsplit=1)[0])
return getattr(module, 'rune_scope_type', default=None)
return getattr(module, 'rune_scope_type', None)
# pylint: disable=bare-except
except: # noqa
return None

def is_scope_instance(self):
'''is this object a scope for `scoped` keys/references'''
if not (scope := self._get_rune_scope_type()):
scope = self._DEFAULT_SCOPE_TYPE
fqcn = f'{self.__class__.__module__}.{self.__class__.__qualname__}'
return fqcn == scope


class ComplexTypeMetaDataMixin(BaseMetaDataMixin):
'''metadata support for complex types'''
Expand All @@ -360,7 +367,7 @@ def deserialize(cls, obj, allowed_meta: set[str]):
'''method used as pydantic `validator`'''
if isinstance(obj, cls):
if cls.meta_checks_enabled():
obj.init_meta(allowed_meta)
obj._init_meta(allowed_meta) # pylint: disable=protected-access
return obj

if isinstance(obj, Reference):
Expand All @@ -387,7 +394,7 @@ def deserialize(cls, obj, allowed_meta: set[str]):
model = rune_cls.model_validate(obj) # type: ignore
model.__dict__[META_CONTAINER] = metadata
if cls.meta_checks_enabled():
model.init_meta(allowed_meta)
model._init_meta(allowed_meta) # pylint: disable=protected-access

# Keys deserialization treatment
model._register_keys(metadata) # pylint: disable=protected-access
Expand Down Expand Up @@ -441,7 +448,7 @@ def deserialize(cls, obj, handler, base_types, allowed_meta: set[str]):
model = cls(data, **obj) # type: ignore
model._register_keys(obj)
if cls.meta_checks_enabled():
model.init_meta(allowed_meta)
model._init_meta(allowed_meta) # pylint: disable=protected-access
return handler(model)

@classmethod
Expand Down Expand Up @@ -553,23 +560,6 @@ def __new__(cls, value, **kwds):
obj.set_meta(check_allowed=False, **kwds)
return obj

# @classmethod
# @lru_cache
# def serializer(cls):
# '''should return the validator for the specific class'''
# ser_fn = partial(cls.serialise, base_type=Decimal)
# return PlainSerializer(ser_fn, return_type=dict)

# @classmethod
# @lru_cache
# def validator(cls, allowed_meta: tuple[str]):
# '''default validator for the specific class'''
# allowed = set(allowed_meta)
# return WrapValidator(partial(cls.deserialize,
# base_types=(Decimal, float, int, str),
# allowed_meta=allowed),
# json_schema_input_type=float | int | str | dict)


class _EnumWrapperDefaultVal(Enum):
'''marker for not set value in enum wrapper'''
Expand Down Expand Up @@ -641,7 +631,7 @@ def deserialize(cls, obj, allowed_meta: set[str]):
model.set_meta(check_allowed=False, **obj)
model._register_keys(obj) # pylint: disable=protected-access
if _EnumWrapper.meta_checks_enabled():
model.init_meta(allowed_meta)
model._init_meta(allowed_meta) # pylint: disable=protected-access
return model

@classmethod
Expand Down
Loading