diff --git a/src/rune/runtime/base_data_class.py b/src/rune/runtime/base_data_class.py index 5e794e8..034bc47 100644 --- a/src/rune/runtime/base_data_class.py +++ b/src/rune/runtime/base_data_class.py @@ -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 @@ -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 @@ -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, *, @@ -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, @@ -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, @@ -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, @@ -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() diff --git a/src/rune/runtime/metadata.py b/src/rune/runtime/metadata.py index 7816e54..9e5b30b 100644 --- a/src/rune/runtime/metadata.py +++ b/src/rune/runtime/metadata.py @@ -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' @@ -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 @@ -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''' @@ -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( @@ -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): @@ -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): @@ -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''' @@ -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): @@ -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 @@ -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 @@ -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''' @@ -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 diff --git a/test/cdm/EUR-Vanilla-account.json b/test/cdm/EUR-Vanilla-account.json new file mode 100644 index 0000000..f918622 --- /dev/null +++ b/test/cdm/EUR-Vanilla-account.json @@ -0,0 +1,476 @@ +{ + "@model": "Just another Rosetta model", + "@type": "cdm.event.common.TradeState", + "@version": "0.0.0.master-SNAPSHOT", + "trade": { + "tradeLot": [ + { + "priceQuantity": [ + { + "price": [ + { + "value": 0.006982, + "unit": { + "currency": { + "@data": "EUR" + } + }, + "perUnitOf": { + "currency": { + "@data": "EUR" + } + }, + "priceType": "InterestRate", + "@type": "cdm.observable.asset.PriceSchedule", + "@key:scoped": "price-1" + } + ], + "quantity": [ + { + "value": 10000000, + "unit": { + "currency": { + "@data": "EUR" + } + }, + "@type": "cdm.base.math.NonNegativeQuantitySchedule", + "@key:scoped": "quantity-2" + } + ] + }, + { + "quantity": [ + { + "value": 10000000, + "unit": { + "currency": { + "@data": "EUR" + } + }, + "@type": "cdm.base.math.NonNegativeQuantitySchedule", + "@key:scoped": "quantity-1" + } + ], + "observable": { + "Index": { + "InterestRateIndex": { + "FloatingRateIndex": { + "identifier": [ + { + "identifier": { + "@data": "EUR-EURIBOR-Reuters" + }, + "identifierType": "Other" + } + ], + "floatingRateIndex": { + "@data": "EUR-EURIBOR-Reuters" + }, + "indexTenor": { + "periodMultiplier": 6, + "period": "M" + }, + "@type": "cdm.observable.asset.FloatingRateIndex" + }, + "@key:scoped": "InterestRateIndex-1" + } + }, + "@key:scoped": "observable-1" + } + } + ] + } + ], + "product": { + "identifier": [ + { + "identifier": { + "@data": "InterestRate:IRSwap:FixedFloat", + "@scheme": "http://www.fpml.org/coding-scheme/product-taxonomy" + }, + "source": "Other" + } + ], + "taxonomy": [ + { + "primaryAssetClass": { + "@data": "InterestRate", + "@scheme": "http://www.fpml.org/coding-scheme/asset-class-simple" + }, + "@type": "cdm.base.staticdata.asset.common.ProductTaxonomy" + }, + { + "source": "ISDA", + "value": { + "name": { + "@data": "InterestRate:IRSwap:FixedFloat", + "@scheme": "http://www.fpml.org/coding-scheme/product-taxonomy" + } + }, + "@type": "cdm.base.staticdata.asset.common.ProductTaxonomy" + }, + { + "source": "ISDA", + "productQualifier": "InterestRate_IRSwap_FixedFloat", + "@type": "cdm.base.staticdata.asset.common.ProductTaxonomy" + } + ], + "economicTerms": { + "payout": [ + { + "InterestRatePayout": { + "payerReceiver": { + "payer": "Party1", + "receiver": "Party2" + }, + "priceQuantity": { + "quantitySchedule": { + "@ref:scoped": "quantity-2" + } + }, + "rateSpecification": { + "FixedRateSpecification": { + "rateSchedule": { + "price": { + "@ref:scoped": "price-1" + } + } + } + }, + "dayCountFraction": { + "@data": "30/360" + }, + "calculationPeriodDates": { + "effectiveDate": { + "adjustableDate": { + "unadjustedDate": "2015-03-06", + "dateAdjustments": { + "businessDayConvention": "NONE" + } + } + }, + "terminationDate": { + "adjustableDate": { + "unadjustedDate": "2025-03-06", + "dateAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + } + } + }, + "calculationPeriodDatesAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + }, + "calculationPeriodFrequency": { + "periodMultiplier": 1, + "period": "Y", + "rollConvention": "6", + "@type": "cdm.base.datetime.CalculationPeriodFrequency" + }, + "@key:external": "fixedCalcPeriodDates1" + }, + "paymentDates": { + "paymentFrequency": { + "periodMultiplier": 1, + "period": "Y" + }, + "payRelativeTo": "CalculationPeriodEndDate", + "paymentDatesAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + }, + "@key:external": "paymentDates1" + }, + "@type": "cdm.product.asset.InterestRatePayout" + } + }, + { + "InterestRatePayout": { + "payerReceiver": { + "payer": "Party2", + "receiver": "Party1" + }, + "priceQuantity": { + "quantitySchedule": { + "@ref:scoped": "quantity-1" + } + }, + "rateSpecification": { + "FloatingRateSpecification": { + "rateOption": { + "@ref:scoped": "InterestRateIndex-1" + }, + "@type": "cdm.product.asset.FloatingRateSpecification" + } + }, + "dayCountFraction": { + "@data": "ACT/360" + }, + "calculationPeriodDates": { + "effectiveDate": { + "adjustableDate": { + "unadjustedDate": "2015-03-06", + "dateAdjustments": { + "businessDayConvention": "NONE" + } + } + }, + "terminationDate": { + "adjustableDate": { + "unadjustedDate": "2025-03-06", + "dateAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + } + } + }, + "calculationPeriodDatesAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + }, + "calculationPeriodFrequency": { + "periodMultiplier": 6, + "period": "M", + "rollConvention": "6", + "@type": "cdm.base.datetime.CalculationPeriodFrequency" + }, + "@key:external": "floatingCalcPeriodDates2" + }, + "paymentDates": { + "paymentFrequency": { + "periodMultiplier": 6, + "period": "M" + }, + "payRelativeTo": "CalculationPeriodEndDate", + "paymentDatesAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + }, + "@key:external": "paymentDates2" + }, + "resetDates": { + "calculationPeriodDatesReference": { + "@ref:external": "floatingCalcPeriodDates2" + }, + "resetRelativeTo": "CalculationPeriodStartDate", + "fixingDates": { + "periodMultiplier": -2, + "period": "D", + "dayType": "Business", + "businessDayConvention": "NONE", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + }, + "dateRelativeTo": { + "@ref": "1163732c", + "@ref:external": "resetDates2" + }, + "@type": "cdm.base.datetime.RelativeDateOffset" + }, + "resetFrequency": { + "periodMultiplier": 6, + "period": "M", + "@type": "cdm.product.common.schedule.ResetFrequency" + }, + "resetDatesAdjustments": { + "businessDayConvention": "MODFOLLOWING", + "businessCenters": { + "businessCenter": [ + { + "@data": "EUTA" + } + ] + } + }, + "@key:external": "resetDates2" + }, + "@type": "cdm.product.asset.InterestRatePayout" + } + } + ] + } + }, + "counterparty": [ + { + "role": "Party1", + "partyReference": { + "@ref:external": "p1" + } + }, + { + "role": "Party2", + "partyReference": { + "@ref:external": "p2" + } + } + ], + "tradeIdentifier": [ + { + "issuer": { + "@data": "54930084UKLVMY22DS16", + "@scheme": "http://www.fpml.org/coding-scheme/external/iso17442" + }, + "assignedIdentifier": [ + { + "identifier": { + "@data": "UITD7895394", + "@scheme": "http://www.fpml.org/coding-scheme/external/uti" + } + } + ], + "identifierType": "UniqueTransactionIdentifier", + "@type": "cdm.event.common.TradeIdentifier" + } + ], + "tradeDate": { + "@data": "2018-01-29" + }, + "party": [ + { + "partyId": [ + { + "identifier": { + "@data": "54930084UKLVMY22DS16", + "@scheme": "http://www.fpml.org/coding-scheme/external/iso17442" + }, + "identifierType": "LEI" + } + ], + "name": { + "@data": "Party A" + }, + "@key:external": "p1" + }, + { + "partyId": [ + { + "identifier": { + "@data": "48750084UKLVTR22DS78", + "@scheme": "http://www.fpml.org/coding-scheme/external/iso17442" + }, + "identifierType": "LEI" + } + ], + "name": { + "@data": "Party B" + }, + "@key:external": "p2" + } + ], + "contractDetails": { + "documentation": [ + { + "legalAgreementIdentification": { + "agreementName": { + "agreementType": "MasterAgreement", + "masterAgreementType": { + "@data": "ISDAMaster", + "@scheme": "http://www.fpml.org/coding-scheme/master-agreement-type" + } + } + }, + "contractualParty": [ + { + "@ref:external": "p1" + }, + { + "@ref:external": "p2" + } + ], + "@type": "cdm.legaldocumentation.common.LegalAgreement" + }, + { + "legalAgreementIdentification": { + "agreementName": { + "agreementType": "Confirmation", + "contractualDefinitionsType": [ + { + "@data": "ISDA2006", + "@scheme": "http://www.fpml.org/coding-scheme/contractual-definitions" + } + ] + } + }, + "contractualParty": [ + { + "@ref:external": "p1" + }, + { + "@ref:external": "p2" + } + ], + "@type": "cdm.legaldocumentation.common.LegalAgreement" + } + ] + }, + "account": [ + { + "partyReference": { + "@ref:external": "p1" + }, + "accountNumber": { + "@data": "p1-account-a" + }, + "accountBeneficiary": { + "@ref:external": "p1" + }, + "@key:external": "p1-acc" + }, + { + "partyReference": { + "@ref:external": "p2" + }, + "accountNumber": { + "@data": "p2-account-a" + }, + "accountBeneficiary": { + "@ref:external": "p2" + }, + "@key:external": "p2-acc" + } + ], + "@type": "cdm.event.common.Trade" + }, + "@key": "3d6d5a8f" +} \ No newline at end of file diff --git a/test/cdm/test_create_irs.py b/test/cdm/test_create_irs.py new file mode 100644 index 0000000..b33b555 --- /dev/null +++ b/test/cdm/test_create_irs.py @@ -0,0 +1,127 @@ +'''create a test irs''' +# pylint: disable=invalid-name +import uuid +import os +from datetime import date +import pytest +from rune.runtime.base_data_class import BaseDataClass +try: + # pylint: disable=unused-import + # type: ignore + from cdm.event.common.Trade import Trade + from cdm.event.common.TradeIdentifier import TradeIdentifier + from cdm.product.template.TradableProduct import TradableProduct + from cdm.product.template.Product import Product + from cdm.product.template.TradeLot import TradeLot + from cdm.observable.asset.PriceQuantity import PriceQuantity + from cdm.base.staticdata.party.Party import Party + from cdm.base.staticdata.party.PartyIdentifier import PartyIdentifier + from cdm.base.staticdata.party.Counterparty import Counterparty + from cdm.base.staticdata.party.CounterpartyRoleEnum import CounterpartyRoleEnum + # from cdm_observable_asset_Index import Index + from cdm.base.staticdata.identifier.AssignedIdentifier import AssignedIdentifier + from cdm.base.staticdata.party.PartyIdentifierTypeEnum import PartyIdentifierTypeEnum + from cdm.event.common.TradeIdentifier import TradeIdentifier + from cdm.base.staticdata.identifier.TradeIdentifierTypeEnum import TradeIdentifierTypeEnum + from cdm.base.staticdata.identifier.AssignedIdentifier import AssignedIdentifier + from cdm.product.template.Product import Product + # from cdm.product.template.ContractualProduct import ContractualProduct + from cdm.base.staticdata.asset.common.ProductTaxonomy import ProductTaxonomy + from cdm.base.staticdata.asset.common.AssetClassEnum import AssetClassEnum + from cdm.base.staticdata.asset.common.TaxonomySourceEnum import TaxonomySourceEnum + from cdm.base.staticdata.asset.common.TaxonomyValue import TaxonomyValue + from cdm.base.staticdata.asset.common.ProductIdentifier import ProductIdentifier + from cdm.base.staticdata.asset.common.ProductIdTypeEnum import ProductIdTypeEnum + from cdm.product.template.EconomicTerms import EconomicTerms + from cdm.product.template.Payout import Payout + from cdm.product.asset.InterestRatePayout import InterestRatePayout + from cdm.base.staticdata.party.PayerReceiver import PayerReceiver + from cdm.base.staticdata.party.CounterpartyRoleEnum import CounterpartyRoleEnum + NO_SER_TEST_MOD = False +except ImportError: + NO_SER_TEST_MOD = True + + +@pytest.mark.skipif(NO_SER_TEST_MOD, reason='CDM package not found') +def test_create_irs(): + '''minimal IRS trade''' + party = [None, None] + partyId = PartyIdentifier(identifier='54930084UKLVMY22DS16', + identifierType=PartyIdentifierTypeEnum.LEI) + partyId.identifier.set_meta( # pylint: disable=no-member + scheme='http://www.fpml.org/coding-scheme/external/iso17442') + party[0] = Party(partyId=[partyId], name='Party A') + + partyId1 = PartyIdentifier(identifier='851WYGNLUQLFZBSYGB56', + identifierType=PartyIdentifierTypeEnum.LEI) + partyId1.identifier.set_meta( # pylint: disable=no-member + scheme='http://www.fpml.org/coding-scheme/external/iso17442') + party[1] = Party(partyId=[partyId1], name='Party B') + + assignedIdentifier = AssignedIdentifier(identifier=str(uuid.uuid4())) + assignedIdentifier.identifier.set_meta( # pylint: disable=no-member + scheme='http://www.fpml.org/coding-scheme/external/uti') + tradeIdentifier = TradeIdentifier( + issuer='54930084UKLVMY22DS16', + assignedIdentifier=[assignedIdentifier], + identifierType=TradeIdentifierTypeEnum.UNIQUE_TRANSACTION_IDENTIFIER) + tradeIdentifier.issuer.set_meta( # pylint: disable=no-member + scheme='http://www.fpml.org/coding-scheme/external/iso17442') + + val = tradeIdentifier.rune_serialize() + + productTaxonomy = [None, None, None] + productTaxonomy[0] = ProductTaxonomy( + primaryAssetClass=AssetClassEnum.INTEREST_RATE) + productTaxonomy[1] = ProductTaxonomy( + source=TaxonomySourceEnum.ISDA, + value=TaxonomyValue(name='InterestRate:IRSwap:FixedFloat')) + productTaxonomy[2] = ProductTaxonomy( + source=TaxonomySourceEnum.ISDA, + productQualifier='InterestRate_IRSwap_FixedFloat') + + productIdentifier = ProductIdentifier( + identifier='InterestRate:IRSwap:FixedFloat', + source=ProductIdTypeEnum.OTHER) + val = productIdentifier.rune_serialize() + + + # priceQuantity = quantitySchedule + + # interestRatePayout = [None, None] + # interestRatePayout[0] = InterestRatePayout( + # payerReceiver=PayerReceiver(payer=CounterpartyRoleEnum.PARTY_1, + # receiver=CounterpartyRoleEnum.PARTY_2)) + + + # payout = Payout() + # economicTerms = EconomicTerms(payout=payout) + + # contractualProduct = ContractualProduct( + # productTaxonomy=productTaxonomy, + # productIdentifier=[productIdentifier], + # economicTerms=economicTerms) + # val = contractualProduct.rune_serialize() + + # cdm_product_template_EconomicTerms + # + # product = Product() + # tradableProduct = TradableProduct() + + # trade = Trade(tradeIdentifier=[tradeIdentifier], + # tradeDate=date.today().isoformat(), + # party=party, + # tradableProduct=tradableProduct) + # val = trade.rune_serialize() + assert val + + +@pytest.mark.skipif(NO_SER_TEST_MOD, reason='CDM package not found') +def test_rune_deserialize(): + '''no doc''' + path = os.path.join(os.path.dirname(__file__), 'EUR-Vanilla-account.json') + fp = open(path, 'rt', encoding='utf-8') + obj = BaseDataClass.rune_deserialize(fp.read(), validate_model=False) + assert obj + +# EOF diff --git a/test/cdm/test_trade_creation.py b/test/cdm/test_trade_creation.py index af418f7..bd6588a 100644 --- a/test/cdm/test_trade_creation.py +++ b/test/cdm/test_trade_creation.py @@ -9,25 +9,27 @@ from cdm.event.common.TradeIdentifier import TradeIdentifier from cdm.product.template.TradableProduct import TradableProduct from cdm.product.template.Product import Product + from cdm.product.template.NonTransferableProduct import NonTransferableProduct from cdm.product.template.TradeLot import TradeLot - from cdm.product.common.settlement.PriceQuantity import PriceQuantity + from cdm.observable.asset.PriceQuantity import PriceQuantity from cdm.base.staticdata.party.Party import Party from cdm.base.staticdata.party.PartyIdentifier import PartyIdentifier from cdm.base.staticdata.party.Counterparty import Counterparty from cdm.base.staticdata.party.CounterpartyRoleEnum import CounterpartyRoleEnum - from cdm.base.staticdata.asset.common.Index import Index + from cdm.observable.asset.Index import Index from cdm.base.staticdata.identifier.AssignedIdentifier import AssignedIdentifier NO_SER_TEST_MOD = False except ImportError: NO_SER_TEST_MOD = True -@pytest.mark.skipif(NO_SER_TEST_MOD, reason='CDM package not found') +# @pytest.mark.skipif(NO_SER_TEST_MOD, reason='CDM package not found') +@pytest.mark.skip(reason='We cannot distinguish CDM 6 vs 5 yet') def test_simple_trade(): '''Constructs a simple Trade in memory and validates the model.''' price_quantity = PriceQuantity() trade_lot = TradeLot(priceQuantity=[price_quantity]) - product = Product(index=Index()) + product = NonTransferableProduct(index=Index()) counterparty = [ Counterparty(role=CounterpartyRoleEnum.PARTY_1, partyReference=Party( diff --git a/test/serializer-round-trip/test_basic.py b/test/serializer-round-trip/test_basic.py index 61dd6eb..2b29122 100644 --- a/test/serializer-round-trip/test_basic.py +++ b/test/serializer-round-trip/test_basic.py @@ -104,7 +104,6 @@ def test_basic_types_single(): } ''' model = Root.model_validate_json(json_str) - model.resolve_references() model.validate_model() @@ -123,7 +122,6 @@ def test_basic_types_list(): } ''' model = Root.model_validate_json(json_str) - model.resolve_references() model.validate_model() # EOF diff --git a/test/serializer-round-trip/test_extension.py b/test/serializer-round-trip/test_extension.py index f70cf5c..e1dcac1 100644 --- a/test/serializer-round-trip/test_extension.py +++ b/test/serializer-round-trip/test_extension.py @@ -105,7 +105,6 @@ def test_temp(): } ''' model = Root.model_validate_json(json_str) - model.resolve_references() assert id(model.nodeRef.typeA) == id(model.nodeRef.aReference) diff --git a/test/serializer-round-trip/test_metakey.py b/test/serializer-round-trip/test_metakey.py index 744f321..cb1ca8a 100644 --- a/test/serializer-round-trip/test_metakey.py +++ b/test/serializer-round-trip/test_metakey.py @@ -106,7 +106,6 @@ def test_attribute_ref(): } ''' model = Root.model_validate_json(json_str) - model.resolve_references() model.validate_model() assert model.attributeRef.dateField == datetime.date(2024, 12, 12) assert model.attributeRef.dateReference == datetime.date(2024, 12, 12) @@ -130,7 +129,6 @@ def test_node_ref(): } ''' model = Root.model_validate_json(json_str) - model.resolve_references() model.validate_model() assert model.nodeRef.typeA.fieldA == 'foo' assert model.nodeRef.aReference.fieldA == 'foo' @@ -149,7 +147,7 @@ def test_dangling_attribute_ref(): } ''' with pytest.raises(KeyError): - Root.model_validate_json(json_str) + Root.rune_deserialize(json_str) def test_dangling_node_ref(): @@ -164,7 +162,7 @@ def test_dangling_node_ref(): } ''' with pytest.raises(KeyError): - Root.model_validate_json(json_str) + Root.rune_deserialize(json_str) @pytest.mark.skipif(NO_SER_TEST_MOD, reason='Generated test package not found') diff --git a/test/test_keys_and_references.py b/test/test_keys_and_references.py index 4235c97..c033f07 100644 --- a/test/test_keys_and_references.py +++ b/test/test_keys_and_references.py @@ -11,6 +11,7 @@ class CashFlow(BaseDataClass): '''test cashflow''' + _ALLOWED_METADATA = {'@key', '@key:external'} currency: str = Field(..., description='currency', min_length=3, @@ -18,6 +19,21 @@ class CashFlow(BaseDataClass): amount: Decimal = Field(..., description='payment amount', ge=0) +class CashFlowNoKey(BaseDataClass): + '''test cashflow''' + currency: str = Field(..., + description='currency', + min_length=3, + max_length=3) + amount: Decimal = Field(..., description='payment amount', ge=0) + + +class DummyLoanNoKey(BaseDataClass): + '''some more complex data structure''' + loan: CashFlowNoKey = Field(..., description='loaned amount') + repayment: CashFlowNoKey = Field(..., description='repaid amount') + + class DummyLoan(BaseDataClass): '''some more complex data structure''' loan: CashFlow = Field(..., description='loaned amount') @@ -124,7 +140,8 @@ def test_use_ref_from_key(): repayment=CashFlow(currency='EUR', amount=101)) key = model.loan.get_or_create_key() # pylint: disable=no-member ref = Reference(key, key_type=KeyType.INTERNAL, parent=model) - model.bind_property_to('repayment', ref) + # pylint: disable=protected-access + model._bind_property_to('repayment', ref) assert id(model.loan) == id(model.repayment) @@ -132,14 +149,15 @@ def test_use_ref_from_object(): '''test use a ref''' model = DummyLoan2(loan=CashFlow(currency='EUR', amount=100), repayment=CashFlow(currency='EUR', amount=101)) - model.bind_property_to('repayment', Reference(model.loan)) + # pylint: disable=protected-access + model._bind_property_to('repayment', Reference(model.loan)) assert id(model.loan) == id(model.repayment) def test_bad_key_generation(): '''generate a key for an object which can't be referenced''' - model = DummyLoan(loan=CashFlow(currency='EUR', amount=100), - repayment=CashFlow(currency='EUR', amount=101)) + model = DummyLoanNoKey(loan=CashFlowNoKey(currency='EUR', amount=100), + repayment=CashFlowNoKey(currency='EUR', amount=101)) with pytest.raises(ValueError): model.loan.get_or_create_key() # pylint: disable=no-member @@ -152,7 +170,8 @@ def test_invalid_property(): repayment=CashFlow(currency='EUR', amount=101)) with pytest.raises(ValueError): - model.bind_property_to('repayment', Reference(model2.loan)) + # pylint: disable=protected-access + model._bind_property_to('repayment', Reference(model2.loan)) def test_ref_assign(): @@ -163,6 +182,13 @@ def test_ref_assign(): assert id(model.loan) == id(model.repayment) +def test_ref_in_constructor(): + '''test use a ref''' + cf = CashFlow(currency='EUR', amount=100) + model = DummyLoan2(loan=cf, repayment=Reference(cf)) + assert id(model.loan) == id(model.repayment) + + def test_ref_re_assign(): '''test use a ref''' model = DummyLoan2(loan=CashFlow(currency='EUR', amount=100), @@ -259,7 +285,6 @@ def test_load_loan_with_key_ref(): "repayment":{"@ref":"cf-1-2"} }''' model = DummyLoan2.model_validate_json(json_str) - model.resolve_references() assert id(model.loan) == id(model.repayment) @@ -270,7 +295,6 @@ def test_load_basic_type_loan_with_key_ref(): "repayment":{"@ref":"8e50b68b-6426-44a8-bbfd-cbe3b833131c"} }''' model = DummyLoan3.model_validate_json(json_str) - model.resolve_references() assert id(model.loan) == id(model.repayment) @@ -281,7 +305,6 @@ def test_load_basic_type_loan_with_key_ref_and_constraints(): "repayment":{"@ref":"8e50b68b-6426-44a8-bbfd-cbe3b833131a"} }''' model = DummyLoan4.model_validate_json(json_str) - model.resolve_references() model.validate_model() assert id(model.loan) == id(model.repayment) @@ -293,7 +316,6 @@ def test_load_basic_type_loan_with_key_ref_and_broken_constraints(): "repayment":{"@ref":"8e50b68b-6426-44a8-bbfd-cbe3b833131b"} }''' model = DummyLoan4.model_validate_json(json_str) - model.resolve_references() with pytest.raises(ValidationError): model.validate_model() diff --git a/test/test_rune_parent.py b/test/test_rune_parent.py index 5468876..0b8bbd6 100644 --- a/test/test_rune_parent.py +++ b/test/test_rune_parent.py @@ -1,7 +1,6 @@ '''test module for rune root lifecycle''' from typing import Optional, Annotated from pydantic import Field -import pytest from rune.runtime.metadata import Reference, KeyType from rune.runtime.base_data_class import BaseDataClass @@ -29,8 +28,31 @@ class Root(BaseDataClass): 'bAddress': {'@ref:scoped'} } + +class Bplus(BaseDataClass): + '''no doc''' + bAddress: Optional[Annotated[B, + B.serializer(), + B.validator(('@ref:scoped', ))]] = Field( + None, description='') + + _KEY_REF_CONSTRAINTS = { + 'bAddress': {'@ref:scoped'} + } + + +class RootDeep(BaseDataClass): + '''no doc''' + typeA: Optional[Annotated[A, A.serializer(), + A.validator()]] = Field(None, description='') + bplus: Optional[Annotated[Bplus, + Bplus.serializer(), + Bplus.validator()]] = Field(None, description='') + + class DeepRef(BaseDataClass): '''no doc''' + _FQRTN = 'test_rune_parent.DeepRef' root: Annotated[Root, Root.serializer(), Root.validator()] = Field(..., description='') @@ -100,15 +122,15 @@ def test_deep2_creation(mocker): def test_root_deserialization(): '''no doc''' rune_dict = { + "bAddress": { + "@ref:scoped": "aKey3" + }, "typeA": { "b": { "@key:scoped": "aKey3", "fieldB": "some b content" } }, - "bAddress": { - "@ref:scoped": "aKey3" - } } root = Root.model_validate(rune_dict) assert root.get_rune_parent() is None @@ -118,19 +140,48 @@ def test_root_deserialization(): assert root.typeA.b == root.bAddress + +def test_root_deep_deserialization(): + '''no doc''' + rune_dict = { + "bplus": { + "bAddress": { + "@ref:scoped": "aKey3" + } + }, + "typeA": { + "b": { + "@key:scoped": "aKey3", + "fieldB": "some b content" + } + }, + # "bplus": { + # "bAddress": { + # "@ref:scoped": "aKey3" + # } + # }, + } + root = RootDeep.rune_deserialize(rune_dict) + assert root.get_rune_parent() is None + assert root == root.typeA.get_rune_parent() + assert root.typeA == root.typeA.b.get_rune_parent() + assert root.typeA == root.bplus.bAddress.get_rune_parent() + assert root.typeA.b == root.bplus.bAddress + + def test_deep_deserialization(): '''no doc''' rune_dict = { "root": { + "bAddress": { + "@ref:scoped": "aKey3" + }, "typeA": { "b": { "@key:scoped": "aKey3", "fieldB": "some b content" } }, - "bAddress": { - "@ref:scoped": "aKey3" - } } } deep = DeepRef.model_validate(rune_dict)